import Logger from '../base/logger';
import { AMSClient, amsErrorEvent, answerCall, connectAmsService, hangupCall, operateEvent } from './ams_client';
import { addStreamToMixStream, recordStream, uploadFile, stopRecordStream, streamOutsToMediaServer, agentLogin, getAgentGroups, refreshNewToken, createTokenWithRtmpServer, agentLoginWithTicketId, externalAgentLogin, addAgentNameInRecordFile, startAsrEvent, agentLoginWithEncrypt } from './api';
import { dispatchEvent, addEventListener, removeEventListener } from '../base/event';
import { refreshAmsTicket, releaseAmsClient, validateAmsClient, resumeAmsConnection } from './ams_client';
import { getBrowserClientInfo, isEmptyString, removeObjectWithId } from './tool';
import { createCameraMicStream, IRtcClient, createScreenStream, joinRoom, leaveRoom, publishStream, stopPublish, republishStream, stopStream, mute, unmute, isEffectiveMediaStreams, replaceMediaStream, renderRemoteStream, zmsAddEventListener, zmsRemoveEventListener, IRTC_CUSTOMEREVENT, renderStreamWithVideoLabel, publicationAddEventListener, TestVideoDevice } from './irtcScript';
import {
	CCSClient,
	connect,
	inviteOtherAgent,
	validateCcsClient,
	joinSession,
	quitSession,
	readyToTalk,
	notifyTransference,
	getCustomerClientInfoWithMembers,
	ccsErrorEvent,
	onCustomMessage,
	sessionMemberJoinQuitEvent,
	onInvitationProgress,
	requestScreenShareWithOthers,
	markScreen,
	onDeviceScreenSize,
	releaseScreenShare,
	releaseCcsClient,
	resumeCcsConnection,
	cancelInstruction,
	onCancelInstruction,
	dealWithCcsSendMessageResponse,
} from './ccs_client';
import { IrtcWhiteBoard } from './whiteboard';
import { systemErrorCode, systemErrorName, ccsErrorName } from '../base/event';
import { mediaOptionsIsValid, defaultMediaOptions, ivcsErrorCodeEnum, validateLocalStreamParameter } from '../base/const';
import { deepCopy } from './tool';
import { IvcsError } from '../base/error';
import { NetworkMonitoring } from '../base/network';
import { Navigator } from '../base/navigator';
import { cancelPeerCmd, onPeerCmdResponse, sendPeerCmd } from './peercmd';
import { ImUpgradeMedia } from './imUpgrade';
import { Snapshot } from '../modules/snapshot';
/**
 * @version 1.21.0
 * @author <changjian.sun,zhiwen.huang,hui.zhang>
 * @type {object} ivcs坐席端相关API
 * {@link https://www.showdoc.com.cn/1499280837202584/7240791073628616|错误码说明}
 * @example 错误码
|  code | message 
| 1000 |参数校验失败  |
| 4000 |网络请求接口失败  |
| 1001 |用户不存在  |
| 1101 |网络请求鉴权失败  |
| 1103 |用户登录密码错误  |
| 1104 |用户账号失效无法登录  |
| 1301 |网络请问参数错误  |
| 1401 |网络请求数据库异常  |
| 5000 |ccsClient未知错误  |
| 5001 |ccsClient未初始化  |
| 5002 |ccsClient-token无效或未初始化  |
| 5003 |ccsClient断开sio链接  |
| 5004 |ccsClient-ticket无效  |
| 5005 |ccsClient-resume失败（重连后刷新链接失败）  |
| 5006 |ccsClient发送消息失败,ccs服务异常,消息无法发送  |
| 5007 |ccsClient初始化链接sio失败  |
| 3000 |amsClient未知错误  |
| 3001 |amsClient未初始化  |
| 3002 |amsClient-token无效或未初始化  |
| 3003 |amsClient断开sio链接  |
| 3004 |amsClient-ticket无效  |
| 3005 |amsClient-resume失败（重连后刷新链接失败）  |
| 3006 |amsClient发送消息失败,ams服务异常,消息无法发送  |
| 3007 |amsClient初始化链接sio失败  |
| 3008 |amsClient发送消息超时  |
| 3009 |amsClient发送消息时，ams正在重连中  |
| 6000 |irtcClient未知错误  |
| 60001 |渲染的流label无效 |
| 6001 |加入房间失败  |
| 6002 |创建本地流失败  |
| 6001 |加入视频房间失败  |
| 6002 |创建视频流失败  |
| 60021 |创建本地流被终止  |
| 60022 |创建本地流不允许，浏览器未授权/系统未授权  |
| 60023 |创建本地流未找到设备  |
| 60024 |创建本地流无法启动设备  |
| 60025 |创建本地流失败，设备分辨率不支持程序设置的分辨率  |
| 60026 |创建本地流失败，安全策略失败，网页链接不安全等  |
| 60027 |创建本地流失败，程序typeError  |
| 6004 |推流失败  |
| 6005 |订阅流失败  |
| 6006 |渲染指定label流的video dom元素不存在  |
| 6007 |视频video自动播放失败  |
| 60070 |mute audio失败  |
| 60071 |mute video失败  |
| 60080 |unmute audio失败  |
| 60081 |unmute video失败  |
| 7000 |SDK内部异常错误  |
 */
class Agent {
	/**
	 * 构造方法
	 * @returns new Agent
	 */
	constructor() {
		this.snapshotModule = new Snapshot();
		this.miniType = 'mini';
		this.networkTool = new NetworkMonitoring();
		this.mediaDevices = new Navigator();
		this.eventList = new Map();
		this.audioInputDeviceID = undefined;
		this.videoInputDeviceID = undefined;
		this.audioConstraints = {
			deviceId: this.audioInputDeviceID,
			source: 'mic',
		};
		this.videoConstraints = {
			deviceId: this.videoInputDeviceID,
			source: 'camera',
			frameRate: 15,
			resolution: { width: 640, height: 480 },
		};

		this.customizedMediaStreamsOptions = null;
		this.loginID = null; // 登录系统唯一ID
		this.participantId = null;
		this.systemRecordId = null; // 系统录像的recordId
		this.recordingIds = []; // 录制相关的recordId
		this.compressedPublicKey = null;
	}

	/**
	 * 测试麦克风是否可用
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败，具体错误原因可参考 {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia}
	 * @example
	 * ivcs.agent.microphoneTest((resp)=>{
		 console.log('success',resp);
		},(error)=>{
		 console.log('error',error);
	   });
	 */
	microphoneTest(onSuccess, onFailure) {
		this.mediaDevices.audioTest(
			resp => {
				onSuccess(resp);
			},
			error => {
				onFailure(error);
			}
		);
	}

	/**
	* 摄像头检测是否可用
	* @param {string} deviceId 摄像头的设备id，默认是传null
	* @param {function} onSuccess 成功
	* @param {function} onFailure 失败，具体错误原因可参考 {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia}
	* @example
	* ivcs.agent.cameraTest(null,(resp)=>{
	   console.log('success',resp);
	   },(error)=>{
	   console.log('error',error);
	   });
	*/
	cameraTest(deviceId = null, onSuccess, onFailure) {
		this.mediaDevices.videoTest(
			deviceId,
			resp => {
				onSuccess(resp);
			},
			error => {
				onFailure(error);
			}
		);
	}

	/**
	* 开启网络测试，并监控网络状态变化
	* @param {function} networkStatus  网络状态变化回调，返回online=网络已链接，返回offline=网络离线
	* @returns {boolean} 当前网络是否正常可用
	* @example 
	* let online = ivcs.agent.startMonitorNetwork((status)=>{
		   console.log('now network status',status);
	   });
	*/
	startMonitorNetwork(networkStatus) {
		this.networkTool.start(status => {
			networkStatus(status);
		});
		return this.networkTool.online;
	}

	/**
	 * 停止网络测试，停止监控网络状况变化
	 * @example
	 * ivcs.agent.stopMonitorNetwork();
	 */
	stopMonitorNetwork() {
		this.networkTool.stop();
	}

	/**
	 * 设置turn server地址，一般在通话前（startVideo）设置。
	 * @param {string} turnserverUrl  turn server地址
	 * @param {string} turnserverCredential turn server的密码
	 * @param {string} turnserverUsername turn server的用户名
	 */
	setTurnServer(turnserverUrl, turnserverCredential, turnserverUsername) {
		if (this.associatedUrls) {
			this.associatedUrls.turnserverUrl = turnserverUrl;
			this.associatedUrls.turnserverCredential = turnserverCredential;
			this.associatedUrls.turnserverUsername = turnserverUsername;
		} else {
			this.associatedUrls = {
				turnserverUrl: turnserverUrl,
				turnserverCredential: turnserverCredential,
				turnserverUsername: turnserverUsername,
			};
		}
	}

	/**
	 * 设置登录密码的加密公钥，由后台/运维人员提供
	 * @param {string} compressedPublicKey
	 */
	setcompressedPublicKey(compressedPublicKey) {
		this.compressedPublicKey = compressedPublicKey;
	}

	/**
	 * 坐席初始化
	 * @param {string} backendUrl 请求的后台服务地址
	 * @param {string} name 用户名
	 * @param {string} password 密码
	 * @param {string} org 所属机构
	 * @param {object} mediaOptions 音视频画面的参数
	 * @param {function} onSuccess 成功，{token: string(accessToken),groupsList: Array(belong-group),rtmpServer:string(rtmpServer),backendUrl:string(backendUrl),user:object({customerLevel,id,isDisable,isRobot,org,profile:{isRobot:boolean()},role,userName,schedulingAttributes:object()})}
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.init(
			loginUrl,
			this.name,
			this.pwd,
			this.org,
			defaultMediaOptions,
			resp => {
				console.log('init success', resp);
			},
			error => {
				console.log(error);
			}
		);
	 */
	async init(backendUrl, name, password, org, mediaOptions = defaultMediaOptions, onSuccess, onFailure) {
		if (backendUrl === undefined || backendUrl === '') {
			Logger.warning('ivcs-js-sdk:backendUrl is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'backendUrl is empty');
			onFailure(error);
			return;
		}
		if (name === undefined || name === '') {
			Logger.warning('ivcs-js-sdk:name is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'name is empty');
			onFailure(error);
			return;
		}
		if (password === undefined || password === '') {
			Logger.warning('ivcs-js-sdk:password is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'password is empty');
			onFailure(error);
			return;
		}
		if (org === undefined || org === '') {
			Logger.warning('ivcs-js-sdk:organization is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'organization is empty');
			onFailure(error);
			return;
		}
		if (mediaOptionsIsValid(mediaOptions) != 'ok') {
			onFailure(mediaOptionsIsValid(mediaOptions));
			return;
		}
		this.mediaOptions = deepCopy(mediaOptions);
		this.backendUrl = deepCopy(backendUrl);
		this.ccsUrl = deepCopy(backendUrl);
		try {
			let login = await agentLogin(backendUrl, name, password, org, this.compressedPublicKey);
			this.associatedUrls = login.data && login.data.data && login.data.data.urls ? login.data.data.urls : {};
			this.accessToken = login.data && login.data.data && login.data.data.accessToken ? login.data.data.accessToken : '';
			this.ticket = login.data && login.data.data && login.data.data.ticket ? login.data.data.ticket : '';
			let tokenData = login.data && login.data.data && login.data.data.tokenData ? login.data.data.tokenData : null;
			this.agentName = tokenData && tokenData.data && tokenData.data.userName ? tokenData.data.userName : 'unknown';
			this.initWithAms(
				tokenData,
				s => {
					onSuccess(s);
				},
				e => {
					onFailure(e);
				}
			);
		} catch (error) {
			onFailure(error);
		}
	}

	/**
	 * 外部系统坐席初始化
	 * @since 1.0.3版本新增API
	 * @param {string} backendUrl 请求的后台服务地址
	 * @param {string} name 用户名
	 * @param {string} password 密码
	 * @param {string} org 所属机构
	 * @param {string} externalType 外系统标志
	 * @param {object} mediaOptions 音视频画面的参数
	 * @param {function} onSuccess 成功，返回SDK相关数据
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.initWithExternal(
			loginUrl,
			this.name,
			this.pwd,
			this.org,
			'ums',
			defaultMediaOptions,
			resp => {
				console.log('init success', resp);
			},
			error => {
				console.log(error);
			}
		);
	 */
	async initWithExternal(backendUrl, name, password, org, externalType, mediaOptions = defaultMediaOptions, onSuccess, onFailure) {
		if (backendUrl === undefined || backendUrl === '') {
			Logger.warning('ivcs-js-sdk:backendUrl is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'backendUrl is empty');
			onFailure(error);
			return;
		}
		if (name === undefined || name === '') {
			Logger.warning('ivcs-js-sdk:name is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'name is empty');
			onFailure(error);
			return;
		}
		if (password === undefined || password === '') {
			Logger.warning('ivcs-js-sdk:password is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'password is empty');
			onFailure(error);
			return;
		}
		if (org === undefined || org === '') {
			Logger.warning('ivcs-js-sdk:organization is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'organization is empty');
			onFailure(error);
			return;
		}
		if (mediaOptionsIsValid(mediaOptions) != 'ok') {
			onFailure(mediaOptionsIsValid(mediaOptions));
			return;
		}
		this.mediaOptions = deepCopy(mediaOptions);
		this.backendUrl = deepCopy(backendUrl);
		this.ccsUrl = deepCopy(backendUrl);
		try {
			let login = await externalAgentLogin(backendUrl, name, password, org, externalType);
			this.associatedUrls = login.data && login.data.data && login.data.data.urls ? login.data.data.urls : {};
			this.accessToken = login.data && login.data.data && login.data.data.accessToken ? login.data.data.accessToken : '';
			this.ticket = login.data && login.data.data && login.data.data.ticket ? login.data.data.ticket : '';
			let tokenData = login.data && login.data.data && login.data.data.tokenData ? login.data.data.tokenData : null;
			this.agentName = tokenData && tokenData.data && tokenData.data.userName ? tokenData.data.userName : 'unknown';
			this.initWithAms(
				tokenData,
				s => {
					onSuccess(s);
				},
				e => {
					onFailure(e);
				}
			);
		} catch (error) {
			onFailure(error);
		}
	}

	/**
	 * 坐席初始化,本API主要针对外系统根据ticketId登录的方式
	 * @since 1.0.3版本新增API
	 * @param {string} backendUrl 请求的后台服务地址
	 * @param {string} ticketId 免登录的票据id
	 * @param {object} mediaOptions 音视频画面的参数
	 * @param {function} onSuccess 成功，返回SDK相关数据
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.initWithTicket(
			loginUrl,
			ticketId,
			defaultMediaOptions,
			resp => {
				console.log('init success', resp);
			},
			error => {
				console.log(error);
			}
		);
	 */
	async initWithTicket(backendUrl, ticketId, mediaOptions = defaultMediaOptions, onSuccess, onFailure) {
		if (ticketId === undefined || ticketId === '' || ticketId == null) {
			Logger.warning('ivcs-js-sdk:ticketId is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'ticketId is empty');
			onFailure(error);
			return;
		}
		if (mediaOptionsIsValid(mediaOptions) != 'ok') {
			onFailure(mediaOptionsIsValid(mediaOptions));
			return;
		}
		this.mediaOptions = deepCopy(mediaOptions);
		this.backendUrl = deepCopy(backendUrl);
		this.ccsUrl = deepCopy(backendUrl);
		try {
			let login = await agentLoginWithTicketId(backendUrl, ticketId);
			this.associatedUrls = login.data && login.data.data && login.data.data.urls ? login.data.data.urls : {};
			this.accessToken = login.data && login.data.data && login.data.data.accessToken ? login.data.data.accessToken : '';
			this.ticket = login.data && login.data.data && login.data.data.ticket ? login.data.data.ticket : '';
			let tokenData = login.data && login.data.data && login.data.data.tokenData ? login.data.data.tokenData : null;
			this.agentName = tokenData && tokenData.data && tokenData.data.userName ? tokenData.data.userName : 'unknown';
			this.initWithAms(
				tokenData,
				s => {
					onSuccess(s);
				},
				e => {
					onFailure(e);
				}
			);
		} catch (error) {
			onFailure(error);
		}
	}

	/**
	 * 坐席初始化,本API主要针对外系统加密登录的方式
	 * @ignore
	 * @since 1.0.8版本新增API
	 * @param {string} backendUrl 请求的后台服务地址
	 * @param {string} name 用户名
	 * @param {string} password 密码
	 * @param {string} org 所属机构
	 * @param {string} encrypt 加密方式 MD5 SHA1 RSA AES DES等
	 * @param {object} mediaOptions 音视频画面的参数
	 * @param {function} onSuccess 成功，返回SDK相关数据
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.initWithEncrypt(
			loginUrl,
			name,
			pwd,
			org,
			encrypt,
			defaultMediaOptions,
			resp => {
				console.log('init success', resp);
			},
			error => {
				console.log(error);
			}
		);
	 */
	async initWithEncrypt(backendUrl, name, password, org, encrypt = '', mediaOptions = defaultMediaOptions, onSuccess, onFailure) {
		if (backendUrl === undefined || backendUrl === '') {
			Logger.warning('ivcs-js-sdk:backendUrl is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'backendUrl is empty');
			onFailure(error);
			return;
		}
		if (name === undefined || name === '') {
			Logger.warning('ivcs-js-sdk:name is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'name is empty');
			onFailure(error);
			return;
		}
		if (password === undefined || password === '') {
			Logger.warning('ivcs-js-sdk:password is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'password is empty');
			onFailure(error);
			return;
		}
		if (org === undefined || org === '') {
			Logger.warning('ivcs-js-sdk:organization is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'organization is empty');
			onFailure(error);
			return;
		}
		if (mediaOptionsIsValid(mediaOptions) != 'ok') {
			onFailure(mediaOptionsIsValid(mediaOptions));
			return;
		}
		if (encrypt) {
			encrypt = encrypt.toLowerCase();
			Logger.debug(`encrypt is ${encrypt}`);
		}
		this.mediaOptions = deepCopy(mediaOptions);
		this.backendUrl = deepCopy(backendUrl);
		this.ccsUrl = deepCopy(backendUrl);
		try {
			let login = await agentLoginWithEncrypt(backendUrl, name, password, org, encrypt, this.compressedPublicKey);
			this.associatedUrls = login.data && login.data.data && login.data.data.urls ? login.data.data.urls : {};
			this.accessToken = login.data && login.data.data && login.data.data.accessToken ? login.data.data.accessToken : '';
			this.ticket = login.data && login.data.data && login.data.data.ticket ? login.data.data.ticket : '';
			let tokenData = login.data && login.data.data && login.data.data.tokenData ? login.data.data.tokenData : null;
			this.agentName = tokenData && tokenData.data && tokenData.data.userName ? tokenData.data.userName : 'unknown';
			let isInitPwd = tokenData && tokenData.data ? tokenData.data.isInitPwd : 'N';
			let pwdUpdateDeadline = tokenData && tokenData.data ? tokenData.data.pwdUpdateDeadline : '';
			let isExpired = tokenData && tokenData.data ? tokenData.data.pwdUpdateDeadline : 'N';

			this.initWithAms(
				tokenData,
				s => {
					onSuccess({ ...s, isInitPwd, pwdUpdateDeadline, isExpired });
				},
				e => {
					onFailure({ ...e, isInitPwd, pwdUpdateDeadline, isExpired });
				}
			);
		} catch (error) {
			onFailure(error);
		}
	}

	/**
	 * 登录系统后，初始化ams链接，并获取业务相关数据
	 * @ignore
	 * @param {object} tokenData 登录后返回的tokenData
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 */
	async initWithAms(tokenData, onSuccess, onFailure) {
		try {
			this.userId = tokenData.data.id;
			this.agentName = tokenData && tokenData.data && tokenData.data.userName ? tokenData.data.userName : 'unknown';
			this.domainType = this.associatedUrls.domainType ? this.associatedUrls.domainType : 'public';
			this.amsUrl = this.backendUrl ? this.backendUrl : this.associatedUrls.amsurl;
			const intervalTimes = 300; // default 5 minutes
			let refreshTicketTimeMs = tokenData.exp - tokenData.iat - intervalTimes > 0 ? (tokenData.exp - tokenData.iat - intervalTimes) * 1000 : intervalTimes * 1000;
			let intervalMax = Math.pow(2, 31) - 1;
			refreshTicketTimeMs > intervalMax ? (refreshTicketTimeMs = intervalMax) : undefined;
			this.refreshTokenInterval = setInterval(async () => {
				try {
					let newTokenTicket = await refreshNewToken(this.backendUrl, this.userId, this.ticket);
					this.ticket = newTokenTicket.ticket;
					this.accessToken = newTokenTicket.accessToken;
					this.amsClient.accessToken = newTokenTicket.accessToken;
				} catch (error) {
					Logger.error('ivcs-js-sdk refreshNewToken error', error, new Date().toISOString());
					clearInterval(this.refreshTokenInterval);
					onFailure(error);
				}
			}, refreshTicketTimeMs);
			let groupList = await getAgentGroups(this.backendUrl, this.accessToken);
			this.amsClient = new AMSClient();
			// eslint-disable-next-line no-unused-vars
			let amsConnect = await connectAmsService(this.amsClient, this.amsUrl, this.accessToken);
			amsErrorEvent(info => {
				dispatchEvent(systemErrorName, systemErrorCode.amsSocketIoError, info);
			});
			onSuccess({
				token: this.accessToken,
				groupsList: groupList,
				rtmpServer: this.associatedUrls.rtmpServer,
				// backendUrl: this.associatedUrls.backendurl,
				user: tokenData.data,
				urls: this.associatedUrls,
			});
		} catch (error) {
			Logger.error('ivcs-js-sdk:init error', error);
			onFailure(error);
		}
	}

	/**
	 * 坐席免登录页面,根据登录api接口返回的数据进行初始化
	 * @param {string} backendUrl 请求的后台服务地址
	 * @param {object} data 登录接口返回的response.data数据。{"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImlkIjo1NiwidXNlck5hbWUiOiIwMDEiLCJvcmciOiIxIiwicm9sZSI6ImFnZW50IiwiaXNEaXNhYmxlZCI6Ik4iLCJpc1JvYm90IjpmYWxzZSwicHJvZmlsZSI6eyJpc1JvYm90IjpmYWxzZX0sImN1c3RvbWVyTGV2ZWwiOm51bGwsIl9pZCI6NTYsInNjaGVkdWxpbmdBdHRyaWJ1dGVzIjp7IuaYr-WQpuacuuWZqOS6uiI6ImZhbHNlIiwi572R54K5IjoiMzMzIn19LCJpYXQiOjE2NzQwMjMzOTEsImV4cCI6MTY3NDA2NjU5MX0.MF6OTHaJIsh-175EUF0m8OYTau4Y7eJ5F1aHTCVcUHU","urls":{"domainType":"private","icbcAxaBackendUrl":"https://localhost:3095","backendurl":"http://ivcs-nginx","amsurl":"http://ivcs-nginx","rtmpServer":"rtmp://localhost:1935/liveshow","turnserverUrl":"turn:localhost?transport=tcp","turnserverCredential":"test","turnserverUsername":"test"},"ticket":"RwupOndY5J_Skh-iK4CnkmyHY6S5EBdyexkK8v0nIKaOIm0uSzN1MSQ4nSgblezL","ticketExpiresIn":24,"tokenData":{"data":{"id":56,"userName":"001","org":"1","role":"agent","isDisabled":"N","isRobot":false,"profile":{"isRobot":false},"customerLevel":null,"_id":56,"schedulingAttributes":{"是否机器人":"false","网点":"333"}},"iat":1674023391,"exp":1674066591}}
	 * @param {object} mediaOptions 音视频画面的参数,可以传undefined。则采用默认参数
	 * @param {function} onSuccess 成功 {token,groupsList,rtmpServer,user,urls}
	 * @param {function} onFailure 失败
	 */
	initWithLoginData(backendUrl, data, mediaOptions = defaultMediaOptions, onSuccess, onFailure) {
		this.backendUrl = deepCopy(backendUrl);
		this.ccsUrl = deepCopy(backendUrl);
		this.mediaOptions = deepCopy(mediaOptions);
		this.accessToken = data.accessToken;
		this.associatedUrls = data.urls;
		this.ticket = data.ticket;
		this.initWithAms(data.tokenData, onSuccess, onFailure);
	}

	/**
	 * 登入
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败 code:['3001','3006','3008','3009']
	 * @example
	 * 	ivcs.agent.login(
			resp => {
				console.log('login success', resp);
				ivcs.agent.onDrop(resp => {
					console.log('on drop resp,', resp);
				});
				ivcs.agent.onRing(data => {
				});
				ivcs.agent.onCancelRing(reason => {
				});
			},
			error => {
				console.log('login error', error);
			}
		);
	 */
	login(onSuccess, onFailure) {
		operateEvent(
			this.amsClient,
			'login',
			resp => {
				this.amsClient.ticket = resp.ticket;
				this.amsClient.refersh_ticket_time = resp.refersh_ticket_time;
				this.loginID = resp.id || '';
				refreshAmsTicket(this.amsClient, this.amsClient.refersh_ticket_time, error => {
					Logger.error('ivcs-js-sdk: refreshAmsTicket error', error);
				});
				onSuccess(resp);
			},
			onFailure
		);
	}

	/**
	 * 登出
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败 code:['3001','3006','3008','3009']
	 * @example
	 * ivcs.agent.logout(
			resp => {
				console.log('logout success', resp);
			},
			error => {
				console.log('logout error', error);
			}
		);
	 */
	logout(onSuccess, onFailure) {
		operateEvent(this.amsClient, 'logout', onSuccess, onFailure, null, () => {
			releaseAmsClient(this.amsClient);
			clearInterval(this.refreshTokenInterval);
		});
	}

	/**
	 * 签入技能组
	 * @param {object} groups 签入技能组id集合
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败 code:['3001','3006','3008','3009']
	 * @example
	 *  let groups = '1';
		let checkInGroups = {
			groups: groups
		};
		ivcs.agent.checkIn(
			checkInGroups,
			resp => {
				console.log('checkin success', resp);

			},
			(error) => {
				console.log('checkin error', error);
			}
		);
	 */
	checkIn(groups, onSuccess, onFailure) {
		operateEvent(this.amsClient, 'checkIn', onSuccess, onFailure, groups);
	}

	/**
	 * 结束话后整理
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败 code:['3001','3006','3008','3009']
	 * @example
		ivcs.agent.afterWorkEnd(
			resp => {
				console.log('afterWorkEnd success', resp);
			},
			(error) => {
				console.log('afterWorkEnd error', error);
			}
		);
	 */
	afterWorkEnd(onSuccess, onFailure) {
		operateEvent(this.amsClient, 'afterWorkEnd', onSuccess, onFailure);
	}

	/**
	 * 签出相应的技能组
	 * @param {object} groups 签出技能组id集合
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败 code:['3001','3006','3008','3009']
	 * @example
	 * let groups = '1';
	   let checkInGroups = {
			groups: groups
		};
		ivcs.agent.checkOut(
			checkInGroups,
			resp => {
				console.log('checkOut success', resp);

			},
			(error) => {
				console.log('checkOut error', error);
			}
		);
	 */
	checkOut(groups, onSuccess, onFailure) {
		operateEvent(this.amsClient, 'checkOut', onSuccess, onFailure, groups);
	}

	/**
	 * 获取排队数
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败 code:['3006' = '发生消息失败','3008' = '发送消息超时','3009' = '正在重连中']
	 * @example
	 * ivcs.agent.getQueueInfo(
			resp => {
				console.log('getQueueInfo result', resp);
				// 	[
				//	{ scenName: 1,scenId: 1, groupId: 1, waitingNum: 0 },
				//	{ scenName: 1,scenId: 2, groupId: 2, waitingNum: 0 },
				//	{ scenName: 1,scenId: 6, groupId: 4, waitingNum: 1 }
				//	]
			},
			error => {
				console.log(error);
			}
		);
	 */
	getQueueInfo(onSuccess, onFailure) {
		operateEvent(this.amsClient, 'getQueueInfo', onSuccess, onFailure);
	}

	/**
	 * 就绪
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败 code:['3001','3006','3008','3009']
	 * @example
	 * ivcs.agent.ready(
			resp => {
				console.log('ready result', resp);
			},
			error => {
				console.log(error);
			}
		);
	 */
	ready(onSuccess, onFailure) {
		operateEvent(this.amsClient, 'ready', onSuccess, onFailure);
	}

	/**
	 * 就绪->未就绪
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败 code:['3001','3006','3008','3009']
	 * @example
	 * ivcs.agent.unReady(
			(status,resp) => {
				console.log(status, resp);
			},
			error => {
				console.log(error);
			}
		);
	 */
	unReady(onSuccess, onFailure) {
		operateEvent(this.amsClient, 'unReady', onSuccess, onFailure);
	}

	/**
	 * 小休
	 * @param {object} reason 小休的原因,格式:{reason:string('')}
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败 code:['3001','3006','3008','3009']
	 * @example
	 * let data = {
					reason: '吃饭'
				};
		ivcs.agent.otherWork(
			data,
			resp => {
				console.log('other work success', resp);
			},
			error => {
				console.log('other work error', error);
			}
		);
	 */
	otherWork(reason, onSuccess, onFailure) {
		operateEvent(this.amsClient, 'otherWork', onSuccess, onFailure, reason);
	}

	/**
	 * 预约
	 * @param {object} data 预约的内容,格式:{appointState:enum('otherWork'|'afterWork'|'notReady'),reason:string('')}
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败 code:['3001','3006','3008','3009']
	 * @example
	 * let data = {
						appointState: appointState,
						reason: '吃饭'
					};
		ivcs.agent.appoint(
			data,
			resp => {
				console.log('appoint success', resp);
			},
			error => {
				console.log('appoint error', error);
			}
		);
	 */
	appoint(data, onSuccess, onFailure) {
		operateEvent(this.amsClient, 'appoint', onSuccess, onFailure, data);
	}

	/**
	 * 设置小休原因
	 * @param {object} reason 小休的原因,格式:{reason:string('')}
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败 code:['3001','3006','3008','3009']
	 * @example
	 * let data = {
					reason: '吃饭'
				};
		ivcs.agent.setOtherWorkReason(
			data,
			resp => {
				console.log('setOtherWorkReason success', resp);
			},
			error => {
				console.log('setOtherWorkReason error', error);
			}
		);
	 */
	setOtherWorkReason(reason, onSuccess, onFailure) {
		operateEvent(this.amsClient, 'setOtherWorkReason', onSuccess, onFailure, reason);
	}

	/**
	 * 监听坐席和坐席管理服务端<ams>的链接的状态变化
	 * @param {function} ack 返回{enum:['connected','reconnect_failed','reconnecting','ping-timeout','ping-connected']}
	 * @example
	 * ivcs.agent.onConnectStatus(status => {
			//开发者的业务处理示例
			'connected':'链接成功/重连成功',//第一次触发表示和ams建立链接,后面触发表示重连成功
			'reconnect_failed':'重连失败',//无法接听，就绪，签入等任何功能,需要重新初始化
			'reconnecting':'正在重连中...'//正在重连中，无法执行任何操作
			'offline':'断网',
			'online':'网络恢复链接',
			'disconnect': ams链接断开
	   });
	 */
	onConnectStatus(ack) {
		this.amsClient.onConnectStatus(ack);
		this.startMonitorNetwork(ack);
	}

	/**
	 * 监听坐席和Ccs会话状态变化
	 * @param {function} ack 返回{enum:['connected','reconnect_failed','reconnecting']}
	 * @example
	 * ivcs.agent.onSessionConnectStatus(status => {
			'connected':'链接成功/重连成功',//第一次触发表示和ccs建立链接,后面触发表示重连成功
			'reconnect_failed':'重连失败',//无法发送任何消息如im和指令
			'reconnecting':'正在重连中...'//正在重连中
			'offline':'断网',
			'online':'网络恢复链接'
	   });
	 */
	onSessionConnectStatus(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onSessionConnectStatus error', isValid);
			return;
		}
		this.ccsClient.onConnectStatus(resp => {
			ack(resp);
		});
		this.startMonitorNetwork(ack);
	}

	/**
	 * 获取会话中的成员列表
	 * @param {string|object} data 扩展字段,现阶段不需要传
	 * @param {function} onSuccess 返回成员列表
	 * @param {function} onFailure 获取失败
	 */
	getSessionMembers(data = null, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:getSessionMembers error', isValid);
			onFailure(isValid);
			return;
		}
		this.ccsClient.getSessionMembers(data, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.info(`ivcs-js-sdk:getSessionMembers error,${resp},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 监听坐席小休状态
	 * @param {function} ack 返回小休状态 {"status":"otherWork","conditionOfOtherwork":"sessionOver" | "timeoutOver"}
	 * @example
	 * ivcs.agent.onResetStatus(data => {
			//开发者的业务处理示例
			if (data && data.status === 'otherWork') {
				this.otherWorkStatus = data.conditionOfOtherwork;
			}
	   });
	 */

	onResetStatus(ack) {
		this.amsClient.onResetStatus(data => {
			Logger.info('ivcs-js-sdk:reset data', data, new Date());
			ack(data);
		});
	}

	/**
	 * 收到振铃消息
	 * @param {function} ack 返回振铃消息内容，包含当前呼叫会话的相关信息 如:{bizScenarioId: "1",room: "",sessionId: "",url: {},userData: {service: "1", specifyAgent: "", userId: "4", callMediaType: "video", receiveMediaType: "video",}
	 * @example
	 * ivcs.agent.onRing(data => {
			//开发者的业务处理示例
			this.ring();
			if (data && data.userData && data.userData.formId) {
				this.$store.state.formId = data.userData.formId;
			}
	   });
	 */
	onRing(ack) {
		this.amsClient.onRing(data => {
			Logger.info('ivcs-js-sdk:ring data', data, new Date());
			this.ringData = deepCopy(data);
			let ccsUrl;
			if (this.domainType == 'public') {
				ccsUrl = new URL(data.url.publicUrl);
			} else if (this.domainType == 'private') {
				ccsUrl = new URL(data.url.privateUrl);
			} else if (this.domainType == 'local') {
				ccsUrl = new URL(data.url.localUrl);
			}
			this.ringData.url = ccsUrl.toString();
			this.sessionId = this.ringData.sessionId;
			window.ivcsSessionId = this.sessionId;
			ack(this.ringData);
		});
	}

	/**
	 * 收到振铃取消事件消息,一般是访客取消(退出)呼叫触发
	 * @param {function} ack 事件回调
	 * ivcs.agent.onCancelRing(reason => {
			//开发者的业务处理示例
			this.stopRing();
			console.log('stop ring,', reason);
		});
	 */
	onCancelRing(ack) {
		this.amsClient.onStopRinging(reason => {
			Logger.info('ivcs-js-sdk:stop ring', reason, new Date());
			this.ringData = {};
			ack(reason);
		});
	}

	/**
	 * 监听会话中的邀请状态。（其他坐席邀请别人的进度可通过本api获取）。需要在坐席接听成功后执行才能生效
	 * @param {function} ack 呼叫的进度回调,状态数据. 如果返回error则说明未加入会话。无法执行监听。
	 */
	onInvitationProgress(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			ack('error');
			return;
		}
		onInvitationProgress(this.ccsClient, resp => {
			Logger.info(`ivcs-js-sdk:invitationProgress result:${resp},time:${new Date().toISOString()}`);
			ack(resp);
		});
	}

	/**
	 * 监听ccs resume-connect 成功
	 * @param {function} ack 返回当前会话状态
	 */
	onCcsResumeSuccess(ack) {
		this.ccsClient.resumeConnectionSuccess(ack);
	}

	/**
	 * 监听ams resume-connect 成功
	 * @param {function} ack 返回当前ams状态
	 */
	onAmsResumeSuccess(ack) {
		this.amsClient.resumeConnectionSuccess(ack);
	}

	/**
	 * 监听ams resume-connect 失败
	 * @param {function} ack 坐席需要重新登入。
	 */
	onAmsResumeError(ack) {
		this.amsClient.resumeConnectionError(ack);
	}

	/**
	 * 媒体切换,常用于im通话升级成语音/视频通话
	 * @param {object} data {media:'im','audio','video'}
	 * @param {function} onSuccess
	 * @param {function} onFailure
	 */
	requestMediaSwitch(data, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.requestMediaSwitch(data, (status, resp) => {
			if (status == 'ok') {
				this.roomToken = resp.roomToken;
				this.roomId = resp.roomId;
				onSuccess(resp);
			} else {
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 监听媒体切换事件
	 * @param {function} ack
	 * {
		  "type": "media-switch",
		  "from": "system",
		  "to": ....,
		  "data": {
			 status: "upgrade",
			 type: "im" | "video" | "audio",
			 room: ...,
			 roomToken: ...,
			 at: ...
		  }
		}
	 */
	onMediaSwitch(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			return;
		}
		this.ccsClient.onMediaSwitch(resp => {
			this.roomId = resp.data && resp.data.roomId ? resp.data.roomId : '';
			this.roomToken = resp.data && resp.data.roomToken ? resp.data.roomToken : '';
			ack(resp);
		});
	}

	/**
	 * 推送本地音视频画面
	 * @param {string} videoLabel 视频标签的标识，可根据这个标识处理业务UI的布局
	 * @param {string} callMediaType 呼叫类型,可选值'audio','video'，默认是视频呼叫
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.startVideo(
					'agent',
					this.callMediaType,
					resp => {
						console.log('start video success', resp);
					},
					error => {
						console.log('start video failed', error);
					}
				);
	 */
	async startVideo(videoLabel, callMediaType = 'video', onSuccess, onFailure) {
		try {
			let turnServerUrl = this.associatedUrls.turnserverUrl;
			let turnServerCredential = this.associatedUrls.turnserverCredential;
			let turnServerUsername = this.associatedUrls.turnserverUsername;
			this.irtcClient = new IRtcClient(turnServerUrl, turnServerCredential, turnServerUsername);
			this.irtcClient.clientInfo.clientId = this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '';
			this.irtcClient.clientInfo.userName = this.agentName;
			this.irtcClient.clientInfo.role = 'agent';
			this.irtcClient.sessionId = deepCopy(this.sessionId);
			this.irtcClient.accessToken = deepCopy(this.accessToken);
			this.irtcClient.backendUrl = deepCopy(this.backendUrl);
			Logger.info(`ivcs-js-sdk:request-progress-start-join-room:${new Date().toISOString()}`);
			this.roomInfo = await joinRoom(this.irtcClient, this.roomId, this.roomToken);
			zmsAddEventListener(this.irtcClient);
			//set subscription data
			this.irtcClient.subscriptionList = new Map();
			let streamOptions = {
				audioConstraints: this.mediaOptions.cameraStreamAudioConstraints,
				videoConstraints: this.mediaOptions.cameraStreamVideoConstraints,
				publishOptions: this.mediaOptions.cameraPublishOptions,
			};
			this.clientVideoLabel = deepCopy(videoLabel);
			Logger.info(`ivcs-js-sdk:request-progress-start-create-stream:${new Date().toISOString()}`);
			let haveAudio = true;
			let haveVideo = callMediaType === 'video' ? true : false;
			let streamInfo = {
				participantId: this.participantId ? this.participantId : '',
				clientId: this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '',
			};
			let userInfo = {
				clientId: this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '',
				userName: this.agentName ? this.agentName : '',
				role: 'agent',
			};
			this.sendStartVideoProgressMessage(videoLabel, 'agent', 'permission');
			let localStream = await createCameraMicStream(this.irtcClient, streamOptions, videoLabel, haveAudio, haveVideo, streamInfo, userInfo);
			Logger.info(`ivcs-js-sdk:request-progress-start-publish-stream:${new Date().toISOString()}`);
			this.sendStartVideoProgressMessage(videoLabel, 'agent', 'publish');
			let publication = await publishStream(this.irtcClient, localStream, streamOptions.publishOptions);
			publicationAddEventListener(this.irtcClient, publication);
			this.ccsClient.sendZmsRoomEvent({ event: 'publish-success' }, (s, r, m) => {
				Logger.info(`ivcs-js-sdk:send zms-room-event publish-success event with status:${s},response:${r},msgId:${m},at:${new Date().toISOString()}`);
			});
			this.ccsClient && this.ccsClient.noticeStreamId({ streamId: publication.id });
			this.publicationStatIntervals = [];
			// eslint-disable-next-line no-unused-vars
			Logger.info(`ivcs-js-sdk:request-progress-start-add-to-mix:${new Date().toISOString()}`);
			await addStreamToMixStream(this.backendUrl, this.roomId, publication.id, this.accessToken);
			this.localPublicationId = publication.id;
			this.publication = publication;
			this.localStream = localStream;
			this.camera = new Navigator();
			this.subscriptionStatIntervals = [];
			this.participantId = this.roomInfo.self.id;
			onSuccess({
				publicationId: publication.id,
				roomInfo: this.roomInfo,
				localStream: this.localStream,
			});
		} catch (e) {
			onFailure(e);
		}
	}

	/**
	 * 发送开启音视频的进度自定义消息
	 * @ignore
	 * @param {string} label 视频标签的标识，可根据这个标识处理业务UI的布局
	 * @param {string} role 'customer' | 'agent'
	 * @param {string} status '  ...' | 'publish'
	 */
	sendStartVideoProgressMessage(label, role, status) {
		const progressData = {
			label: label,
			clientId: this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '',
			role: role || 'agent',
			isRobotStream: false,
			status: status,
			businessType: 'notice_media_progress',
			userName: this.agentName ? this.agentName : '',
		};
		this.sendCustomMessage(
			progressData,
			'others',
			s => {
				Logger.info(`ivcs-js-sdk:sendCustomMessage notice start-video progress success, with status:${status}`);
			},
			e => {
				Logger.error(`ivcs-js-sdk:ivcs-js-sdk:sendCustomMessage notice start-video progress error:${e}, with status:${status}`);
			}
		);
	}

	/**
	 * 坐席端根据参数重新 publish 流
	 * @param {*} options publish 参数 options:{ audio: [ { codec: { name: 'opus' }, maxBitrate: 64 } ],  video: [ { codec: { name: 'vp8' }, maxBitrate: 1024 } ] }
	 * @param {*} onSuccess 成功回调 { publicationId: 发布者流ID, roomInfo: 房间信息, localStream: 重新发布后的流 }
	 * @param {*} onFailure 失败回调 { code: 失败代码, message: 具体原因 }
	 * @example
	 * ivcs.agent.republishStreamWithOptions(
					options,
					this.callMediaType,
					resp => {
						console.log('start video success', resp);
					},
					error => {
						console.log('start video failed', error);
					}
				);
	 */
	async republishStreamWithOptions(options, onSuccess, onFailure) {
		try {
			Logger.info(`ivcs-js-sdk:republish start: ${JSON.stringify(options)} ${new Date().toISOString()}`);
			let publication = await republishStream(this.publication, this.irtcClient, this.localStream, options);
			if (publication) {
				this.localPublicationId = publication.id;
				this.publication = publication;
				await addStreamToMixStream(this.backendUrl, this.roomId, this.publication.id, this.accessToken);
				Logger.info(`ivcs-js-sdk:republish success publication: ${JSON.stringify(this.publication)} ${new Date().toISOString()}`);
				onSuccess({
					publicationId: this.publication.id,
					roomInfo: this.roomInfo,
					localStream: this.localStream,
				});
			} else {
				onFailure({ code: 400, message: 'republish failed publication is null' });
			}
		} catch (error) {
			onFailure({ code: 400, messgae: JSON.stringify(error) });
		}
	}
	/**
	 * 坐席关闭网页, 只允许同步任务执行
	 * @param  {function}  ack 回调
	 * @example
	 * ivcs.agent.userPageClose(data => {
	 * 	console.log('user page close,', data);
	 * });
	 */
	userPageClose(ack) {
		try {
			if (this.ccsClient) {
				this.pageClosedOffline(ack);
			} else {
				this.logout(
					resp => ack(resp),
					error => ack(error)
				);
			}
		} catch (error) {
			ack(error);
		}
	}

	/**
	 * 坐席关闭页面时，触发坐席离线
	 * @param  {function}  ack 回调
	 * @example
	 * ivcs.agent.pageClosedOffline(ack => {
	 * 	console.log('page closed offline,', ack);
	 * });
	 */

	pageClosedOffline(ack) {
		this.ccsClient.offline({ roomId: this.roomId, participantId: this.participantId, agentId: this.loginID }, resp => ack(resp));
	}

	/**
	 * 接听访客im呼叫
	 * @param {function} onSuccess 接听成功
	 * @param {function} onFailure 接听失败 {'1017':'访客已离开会话'}
	 * @example
	 * ivcs.agent.answerImCall(
				info => {
					console.log('answer result,', info);
				},
				error => {
					console.log('answer call error', error);
				}
			);
	 */
	async answerImCall(onSuccess, onFailure) {
		if (!this.ringData.url) {
			Logger.warning(`ivcs-js-sdk:no ring or ring stop,${new Date()}`);
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'no ring or ring stop');
			onFailure(error);
			return;
		}
		try {
			let sessionId = this.ringData.sessionId;
			let invitationId = this.ringData.invitationId ? this.ringData.invitationId : '';
			let accessToken = this.accessToken;
			let joinData = {
				media: 'im',
				userData: {
					skill: '',
					specifyAgent: '',
				},
				sessionId: sessionId,
				service: this.ringData.bizScenarioId,
				clientInfo: getBrowserClientInfo(),
				invitationId: invitationId,
			};
			Logger.info(`ivcs-js-sdk:answer-start-answerCall:${new Date().toISOString()}`);
			let answerTask = setTimeout(async () => {
				try {
					// eslint-disable-next-line no-unused-vars
					let answerCallResult = await answerCall(this.amsClient);
					this.ccsClient = new CCSClient();
					this.imUpgradeMedia = new ImUpgradeMedia();
					Logger.info(`ivcs-js-sdk:answer-start-connect-ccs:${new Date().toISOString()}`);
					// eslint-disable-next-line no-unused-vars
					let connectCcsResult = await connect(this.ccsClient, this.ccsUrl, accessToken, error => {
						clearTimeout(answerTask);
					});
					ccsErrorEvent(info => {
						Logger.warning(`ivcs-js-sdk:css-error-event: ${JSON.stringify(info)}  ${new Date().toISOString()}`);
						releaseCcsClient(this.ccsClient);
						dispatchEvent(ccsErrorName, systemErrorCode.ccsSocketIoError, info);
						clearTimeout(answerTask);
					});
					let customerSendMessage = [];
					this.onTextMessage(message => {
						customerSendMessage.push(message);
					});
					Logger.info(`ivcs-js-sdk:answer-start-joinsession:${new Date().toISOString()}`);
					this.joinResult = await joinSession(this.ccsClient, joinData);
					this.roomId = this.joinResult.roomId;
					this.roomToken = this.joinResult.roomToken;
					this.ccsClient.members = this.joinResult.members;
					sessionMemberJoinQuitEvent(
						this.ccsClient,
						quitCustomer => {
							//customer hangup,the room has been removed and the CCS connection has been disconnected
							this.onCustomerQuitSession(quitCustomer);
							//customer quit session while answer not success
							if (!this.ccsClient.members) {
								onFailure(new IvcsError('1017', 'customer quit session'));
							}
							return;
						},
						joinAgent => {
							this.ccsClient.members = [...this.ccsClient.members, joinAgent];
						},
						joinCustomer => {
							this.ccsClient.members = [...this.ccsClient.members, joinCustomer];
						}
					);
					this.ccsClient.onSessionIdChange(data => {
						if (data?.data?.sessionId && this.irtcClient) {
							this.irtcClient.setAllSubscribeCallQuality(deepCopy(this.irtcClient.sessionId));
							this.irtcClient.sessionId = data.data.sessionId;
							this.systemRecordId = null;
						}
					});
					//Under normal circumstances, the number of users in the current session is greater than or equal to 2
					if (this.joinResult.members && this.joinResult.members.length > 1) {
						onSuccess({
							sessionId: sessionId,
							sessionData: this.joinResult,
							customerSendMessage: customerSendMessage,
							customerClientType: this.joinResult.customerClientType,
						});
						return;
					} else {
						onFailure(new IvcsError('1017', 'there is no other user in the current session'));
						return;
					}
				} catch (e) {
					Logger.info(`ivcs-js-sdk:answer-error:${new Date().toISOString()},error:${e}`);
					onFailure(e);
					return;
				}
			});
		} catch (error) {
			Logger.info(`ivcs-js-sdk:answer-error:${new Date().toISOString()},error:${error}`);
			onFailure(error);
			return;
		}
	}

	/**
	 * 收到访客离开会话后的处理
	 * @ignore
	 * @param {object} user 离开的访客
	 */
	onCustomerQuitSession(user) {
		Logger.info('ivcs-js-sdk:customer quitSession with info', user);
		if (this.ccsClient && this.ccsClient.members) {
			let customerList = this.ccsClient.members.filter(item => {
				return item.role === 'customer';
			});
			if (customerList.length > 1) {
				removeObjectWithId(this.ccsClient.members, user.id);
				Logger.info('ivcs-js-sdk:session exist other Customer,members', this.ccsClient.members);
				return;
			}
		}
		zmsRemoveEventListener(this.irtcClient);
		this.stopLocalStream();
	}

	/**
	 * 连接异常，被系统强制结束或者其他网络原因，触发后坐席端无法正常处理会话，需要重新初始化
	 * @param {function} ack 返回具体的异常原因
	 * @example
	 * ivcs.agent.onDrop(resp => {
		console.log('on drop resp,', resp);
		if (resp.code === 403) {
			//账号被其他坐席登录
		} else if (resp.code === 401) {
			//被强制下线
		} else if (resp.code === 402) {
			//ams的socketio链接建立失败
		} else if (resp.code === 404) {
			//ams的socketio链接断开
		} else if (resp.code === 405) {
			//ams的socketio重连失败
		}
	});
	 */
	onDrop(ack) {
		let dropMessage = {
			drop: {
				code: 401,
				message: 'connect forced termination drop',
				reason: 'system',
			},
			error: {
				code: 402,
				message: 'socket io connect error',
			},
			replace: {
				code: 403,
				message: 'other person login,the current account login is invalid',
			},
			onDisconnect: {
				code: 404,
				message: 'ams socketio is disconnected',
			},
			onReconnectFailed: {
				code: 405,
				message: 'ams socketio is reconnect failed',
			},
		};
		this.amsClient.onDrop(reason => {
			Logger.warning('ivcs-js-sdk:ams connect forced termination drop', new Date());
			releaseAmsClient(this.amsClient);
			dropMessage.drop.reason = reason;
			ack(dropMessage.drop);
		});
		this.amsClient.onError(async data => {
			Logger.warning('ivcs-js-sdk:ams connect error,need to stop current business processing', JSON.stringify(data), new Date());
			if (data && data.code === '1101') {
				//token expired
				let newTokenTicket = await refreshNewToken(this.backendUrl, this.userId, this.ticket);
				this.ticket = newTokenTicket.ticket;
				this.accessToken = newTokenTicket.accessToken;
				// eslint-disable-next-line no-unused-vars
				let newConnect = await connectAmsService(this.amsClient, this.amsUrl, this.accessToken);
				// eslint-disable-next-line no-unused-vars
				let resumeConnect = resumeAmsConnection(this.amsClient, this.amsClient.ticket);
			} else {
				releaseAmsClient(this.amsClient);
				ack(dropMessage.error);
			}
		});
		this.amsClient.onReplaced(() => {
			Logger.info('ivcs-js-sdk:other person login,the current account login is invalid, agent that are currently logged in need to be notified, and login again', new Date());
			releaseAmsClient(this.amsClient);
			ack(dropMessage.replace);
		});
		this.amsClient.onDisconnect(() => {
			releaseAmsClient(this.amsClient);
			ack(dropMessage.onDisconnect);
		});
		this.amsClient.onReconnectFailed(() => {
			releaseAmsClient(this.amsClient);
			ack(dropMessage.onReconnectFailed);
		});
	}

	/**
	 * 当前会话踢出其他人
	 * @param {string} clientId 被踢用户在会话中唯一id
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.dropClient(
				clientId,
				function(status, resp) {
					console.log('drop client result', status, resp);
				},
				function(reason) {
					console.log(reason);
				}
		);
	 */
	dropClient(clientId, onSuccess, onFailure) {
		if (isEmptyString(clientId)) {
			Logger.warning(`ivcs-js-sdk:clientId is invalid`);
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'clientId is a required parameter');
			onFailure(error);
			return;
		}
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
		}
		this.ccsClient.dropClient(clientId, (status, resp) => {
			if (status === 'ok') {
				Logger.info(`ivcs-js-sdk:ams dropClient clientId:${clientId} success`, resp, new Date());
				onSuccess('ok', 'success');
			} else {
				Logger.error(`ivcs-js-sdk:ivcs-js-sdk:ams dropClient clientId:${clientId} error`, resp, new Date());
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 强行加入一通会话,<目前适用于班组长坐席强行插入一笔正在进行中的会话>
	 * @param {string} sessionId  需要加入的会话sessionId
	 * @param {string} bizScenarioId  需要加入的会话的业务场景id
	 * @param {function} onSuccess  成功
	 * @param {function} onFailure  失败 {'5008':会话已结束, '5012':会话正在业务场景转接中，无法加入, '5017':'无其他人在会话中'} 其他错误参考错误码
	 * @param {function} onJoinSessionSuccess  joinSession成功,返回当前session的会话详情。{sessionId,sessionData,customerClientType,customerSendMessage}
	 * @example
	 * ivcs.agent.joinSession(
				sessionId,
				bizScenarioId,
				function(resp) {
					console.log('join session success', resp);
				},
				function(error) {
					console.log('join session error', error);
				},
				function(data) {
					console.log('join session data', data);
				}
			);
	*/
	async joinSession(sessionId, bizScenarioId, onSuccess, onFailure, onJoinSessionSuccess) {
		if (isEmptyString(sessionId)) {
			Logger.warning(`ivcs-js-sdk:sessionId is invalid`);
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'sessionId is a required parameter');
			onFailure(error);
			return;
		}
		try {
			let ccsUrl = this.backendUrl;
			let accessToken = this.accessToken;
			let joinSessionTask = setTimeout(async () => {
				try {
					this.ccsClient = new CCSClient();
					this.imUpgradeMedia = new ImUpgradeMedia();
					// eslint-disable-next-line no-unused-vars
					let connectCcsResult = await connect(this.ccsClient, ccsUrl, accessToken, error => {
						clearTimeout(joinSessionTask);
					});
					ccsErrorEvent(info => {
						Logger.info(`ivcs-js-sdk:join-session, css-error-event: ${JSON.stringify(info)}  ${new Date().toISOString()}`);
						releaseCcsClient(this.ccsClient);
						dispatchEvent(ccsErrorName, systemErrorCode.ccsSocketIoError, info);
						clearTimeout(joinSessionTask);
					});
					//访客在等待坐席时发送的消息记录
					let customerSendMessage = [];
					this.onTextMessage(message => {
						customerSendMessage.push(message);
					});
					let joinData = {
						media: 'video',
						userData: {
							skill: '',
							specifyAgent: '',
						},
						sessionId: sessionId,
						service: bizScenarioId,
						clientInfo: getBrowserClientInfo(),
						type: 'force',
					};
					this.sessionId = sessionId;
					window.ivcsSessionId = this.sessionId;
					this.joinResult = await joinSession(this.ccsClient, joinData);
					onJoinSessionSuccess &&
						onJoinSessionSuccess({
							sessionId: sessionId,
							sessionData: this.joinResult,
							customerSendMessage: customerSendMessage,
							customerClientType: this.joinResult.customerClientType,
						});
					if (this.joinResult.members && this.joinResult.members.length > 1) {
						this.ccsClient.onSessionIdChange(data => {
							if (data?.data?.sessionId && this.irtcClient) {
								this.irtcClient.setAllSubscribeCallQuality(deepCopy(this.irtcClient.sessionId));
								this.irtcClient.sessionId = data.data.sessionId;
								this.systemRecordId = null;
							}
						});
						this.systemRecordId = this.joinResult.recordingId;
						this.recordingIds.push(this.systemRecordId);
						onSuccess({
							sessionId: sessionId,
							sessionData: this.joinResult,
							customerSendMessage: customerSendMessage,
							customerClientType: this.joinResult.customerClientType,
						});
						this.ccsClient.members = this.joinResult.members;
						sessionMemberJoinQuitEvent(
							this.ccsClient,
							quitCustomer => {
								this.onCustomerQuitSession(quitCustomer);
							},
							joinAgent => {
								this.ccsClient.members = [...this.ccsClient.members, joinAgent];
							},
							joinCustomer => {
								this.ccsClient.members = [...this.ccsClient.members, joinCustomer];
							}
						);
					} else {
						Logger.info('ivcs-js-sdk: join session error there is no other user in the current session', new Date().toISOString());
						onFailure(new IvcsError('1017', 'there is no other user in the current session'));
						return;
					}
				} catch (error) {
					Logger.error('ivcs-js-sdk: join session error', error, new Date().toISOString());
					onFailure(error);
				}
			});
		} catch (e) {
			Logger.error('ivcs-js-sdk: join session error', e, new Date().toISOString());
			onFailure(e);
		}
	}

	/**
	 * 接听呼叫
	 * @param {string} videoLabel  视频标签
	 * @param {string} callMediaType  接听的呼叫类型（默认是video）
	 * @param {object} customizedMediaStreams  定制化的mediaStream,默认情况下传undefined
	 * @param {object | null} mediaStreamConstraints  MediaStreamConstraints 对象 或者 null
	 * @param {function} onSuccess 接听成功
	 * @param {function} onFailure 接听失败 {'1017':'访客已离开会话'}
	 * @example
	 * ivcs.agent.answer(
				agentLabel,//坐席自己的视频标签
				this.receiveMediaType,//坐席接听的类型，根据访客呼叫的时指定的receiveMediaType来设置
				this.customizedMediaStream,//一般用浏览器自己创建的mediaStream，即undefined
				info => {
					console.log('answer result,', info);
				},
				error => {
					console.log('answer call error', error);
				}
			);
	 */
	async answer(videoLabel, callMediaType = 'video', customizedMediaStreams, mediaStreamConstraints, onSuccess, onFailure) {
		if (isEmptyString(videoLabel)) {
			Logger.warning(`ivcs-js-sdk:The videoLabel is used to tag the current role information,videoLabel is invalid`);
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'videoLabel is a required parameter');
			onFailure(error);
			return;
		}
		if (!this.ringData.url) {
			Logger.warning(`ivcs-js-sdk:no ring or ring stop,${new Date()}`);
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'no ring or ring stop');
			onFailure(error);
			return;
		}
		if (customizedMediaStreams) {
			if (!isEffectiveMediaStreams(customizedMediaStreams)) {
				Logger.warning(`ivcs-js-sdk:customizedMediaStreams is invalid`);
				let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'customized MediaStreams is invalid');
				onFailure(error);
				return;
			}
		}

		try {
			let sessionId = this.ringData.sessionId;
			let invitationId = this.ringData.invitationId ? this.ringData.invitationId : '';
			let accessToken = this.accessToken;
			let joinData = {
				media: 'video',
				userData: {
					skill: '',
					specifyAgent: '',
				},
				sessionId: sessionId,
				service: this.ringData.bizScenarioId,
				clientInfo: getBrowserClientInfo(),
				invitationId: invitationId,
			};
			let backendUrl = this.backendUrl;
			let turnServerUrl = this.associatedUrls.turnserverUrl;
			let turnServerCredential = this.associatedUrls.turnserverCredential;
			let turnServerUsername = this.associatedUrls.turnserverUsername;

			Logger.info(`ivcs-js-sdk:answer-start-answerCall:${new Date().toISOString()}`);
			let answerTask = setTimeout(async () => {
				try {
					// eslint-disable-next-line no-unused-vars
					let answerCallResult = await answerCall(this.amsClient);

					this.ccsClient = new CCSClient();
					this.imUpgradeMedia = new ImUpgradeMedia();
					Logger.info(`ivcs-js-sdk:answer-start-connect-ccs:${new Date().toISOString()}`);
					// eslint-disable-next-line no-unused-vars
					let connectCcsResult = await connect(this.ccsClient, this.ccsUrl, accessToken, error => {
						clearTimeout(answerTask);
					});
					ccsErrorEvent(info => {
						Logger.warning(`ivcs-js-sdk:css-error-event: ${JSON.stringify(info)}  ${new Date().toISOString()}`);
						releaseCcsClient(this.ccsClient);
						dispatchEvent(ccsErrorName, systemErrorCode.ccsSocketIoError, info);
						clearTimeout(answerTask);
					});
					let customerSendMessage = [];
					this.onTextMessage(message => {
						customerSendMessage.push(message);
					});
					Logger.info(`ivcs-js-sdk:answer-start-joinsession:${new Date().toISOString()}`);
					this.joinResult = await joinSession(this.ccsClient, joinData);
					this.ccsClient.members = this.joinResult.members;
					sessionMemberJoinQuitEvent(
						this.ccsClient,
						quitCustomer => {
							//customer hangup,the room has been removed and the CCS connection has been disconnected
							this.onCustomerQuitSession(quitCustomer);
							//customer quit session while answer not success
							if (!this.ccsClient.members) {
								onFailure(new IvcsError('1017', 'customer quit session'));
							}
							return;
						},
						joinAgent => {
							this.ccsClient.members = [...this.ccsClient.members, joinAgent];
						},
						joinCustomer => {
							this.ccsClient.members = [...this.ccsClient.members, joinCustomer];
						}
					);
					//Under normal circumstances, the number of users in the current session is greater than or equal to 2
					if (this.joinResult.members && this.joinResult.members.length > 1) {
						Logger.info(`ivcs-js-sdk:answer-start-create-stream:${new Date().toISOString()}`);
						this.irtcClient = new IRtcClient(turnServerUrl, turnServerCredential, turnServerUsername);
						this.irtcClient.clientInfo.clientId = this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '';
						this.irtcClient.clientInfo.userName = this.agentName;
						this.irtcClient.clientInfo.role = 'agent';
						this.irtcClient.sessionId = deepCopy(this.sessionId);
						this.irtcClient.accessToken = deepCopy(this.accessToken);
						this.irtcClient.backendUrl = deepCopy(this.backendUrl);
						let haveAudio = true;
						let haveVideo = callMediaType === 'video' ? true : false;
						let streamInfo = {
							participantId: this.participantId ? this.participantId : '',
							clientId: this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '',
						};
						let userInfo = {
							clientId: this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '',
							userName: this.agentName ? this.agentName : '',
							role: 'agent',
						};
						let streamOptions = {
							audioConstraints: validateLocalStreamParameter(mediaStreamConstraints) === 'ok' ? mediaStreamConstraints.audioConstraints : this.mediaOptions.cameraStreamAudioConstraints,
							videoConstraints: validateLocalStreamParameter(mediaStreamConstraints) === 'ok' ? mediaStreamConstraints.videoConstraints : this.mediaOptions.cameraStreamVideoConstraints,
							cameraPublishOptions: validateLocalStreamParameter(mediaStreamConstraints) === 'ok' ? mediaStreamConstraints.publishOptions : this.mediaOptions.cameraPublishOptions,
						};

						this.customizedMediaStreamsOptions = streamOptions;

						console.log('validateLocalStreamParameter:', mediaStreamConstraints, validateLocalStreamParameter(mediaStreamConstraints));

						this.localStream = customizedMediaStreams ? customizedMediaStreams : await createCameraMicStream(this.irtcClient, streamOptions, videoLabel, haveAudio, haveVideo, streamInfo, userInfo);
						Logger.info(`ivcs-js-sdk:answer-start-join-room:${new Date().toISOString()}`);
						this.roomId = this.joinResult.roomId;
						this.roomToken = this.joinResult.roomToken;
						this.roomInfo = await joinRoom(this.irtcClient, this.roomId, this.joinResult.roomToken);
						zmsAddEventListener(this.irtcClient);
						this.roomId = this.roomInfo.id;
						Logger.info(`ivcs-js-sdk:answer-start-publish-stream:${new Date().toISOString()}`);
						let publication = await publishStream(this.irtcClient, this.localStream, streamOptions.cameraPublishOptions);
						publicationAddEventListener(this.irtcClient, publication);
						this.ccsClient.sendZmsRoomEvent({ event: 'publish-success' }, (s, r, m) => {
							Logger.info(`ivcs-js-sdk:send zms-room-event publish-success event with status:${s},response:${r},msgId:${m},at:${new Date().toISOString()}`);
						});
						this.ccsClient && this.ccsClient.noticeStreamId({ streamId: publication.id });
						this.publication = publication;
						let participantId = this.roomInfo.self.id;
						this.participantId = participantId;
						Logger.info(`ivcs-js-sdk:answer-start-reasy-to-talk:${new Date().toISOString()}`);
						// eslint-disable-next-line no-unused-vars
						let readyToTalkResult = await readyToTalk(participantId, {}, 'customers', this.ccsClient);
						this.joinResult.customerClientType = getCustomerClientInfoWithMembers(this.joinResult.members);
						if (this.joinResult.customerClientType == this.miniType) {
							Logger.info(`ivcs-js-sdk:answer-start-createTokenWithRtmpServer:${new Date().toISOString()}`);
							let rtmpToken = await createTokenWithRtmpServer(backendUrl, this.accessToken, 60);
							let streamOutsUrl = this.associatedUrls.rtmpServer + '/' + this.joinResult.clientId + this.joinResult.sessionId + '?token=' + rtmpToken;
							// 小程序纯音频模式下，video设为false
							const videoId = callMediaType != 'video' ? null : publication.id;
							Logger.info(`ivcs-js-sdk:answer-start-streamOutsToMediaServer:${new Date().toISOString()}`);
							let streamOuts = await streamOutsToMediaServer(backendUrl, this.roomId, publication.id, videoId, streamOutsUrl);
							// 缓存数据用于纯语音推流转换到音视频推流
							this.joinResult.callMediaType = callMediaType;
							this.joinResult.audioId = publication.id;
							this.joinResult.videoId = publication.id;
							this.joinResult.streamOutsUrl = streamOutsUrl;
							this.joinResult.streamingOutsId = streamOuts.data.id;
						}
						Logger.info(`ivcs-js-sdk:answer-start-addStreamToMixStream:${new Date().toISOString()}`);
						// eslint-disable-next-line no-unused-vars
						let addStreamToMix = await addStreamToMixStream(backendUrl, this.roomId, publication.id, accessToken);
						// eslint-disable-next-line no-unused-vars
						let mixStreamId = this.irtcClient.mixStreamId;
						this.localPublicationId = publication.id;
						this.ccsClient.onSessionIdChange(data => {
							if (data?.data?.sessionId && this.irtcClient) {
								this.irtcClient.setAllSubscribeCallQuality(deepCopy(this.irtcClient.sessionId));
								this.irtcClient.sessionId = data.data.sessionId;
								this.systemRecordId = null;
							}
						});
						this.systemRecordId = this.joinResult.recordingId;
						this.recordingIds.push(this.systemRecordId);
						onSuccess({
							sessionId: sessionId,
							sessionData: this.joinResult,
							customerSendMessage: customerSendMessage,
							customerClientType: this.joinResult.customerClientType,
							publicationId: publication.id,
							participantId: this.roomInfo.self.id,
						});
						this.ccsClient.members = this.joinResult.members;
						Logger.info(`ivcs-js-sdk:answer-success:${new Date().toISOString()}`);

						/**临时解决方案，修复将当前坐席流加入mix流后，并未立即生效，导致无法将坐席工号贴到mix流中。加入1秒延时，确保坐席流成功加入到mix流中 */
						setTimeout(() => {
							Logger.info(`ivcs-js-sdk:notify-stream-add-agentName:${new Date().toISOString()}`);
							addAgentNameInRecordFile(backendUrl, sessionId, this.localPublicationId, this.agentName, accessToken);
						}, 1000);
						//warning:recordings of mix streams need to be placed in the background
						this.streamAddListener = addEventListener(IRTC_CUSTOMEREVENT.STREAMADD, info => {
							Logger.info('ivcs-js-sdk:new stream add with info:', info, ' at:', new Date());
							if (info && info.data) {
								Logger.info('ivcs-js-sdk:new stream add with stream:', info.data, ' at:', new Date());
								//添加asr的监听
								startAsrEvent(this.backendUrl, this.roomId, info.data.id, this.accessToken, participantId, 'baidu');
							}
						});
						this.streamAsrListener = addEventListener(IRTC_CUSTOMEREVENT.ASR, info => {
							Logger.info('ivcs-js-sdk:new stream asr event:', info, ' at:', new Date());
							if (info && info.data) {
								Logger.info('ivcs-js-sdk:new stream asr event:', info.data, ' at:', new Date());
								this.sendTextMessageWithFromClientId(
									info.data.content,
									info.data.clientId,
									'all',
									s => {
										Logger.info('ivcs-js-sdk:send asr text message success', s);
									},
									e => {
										Logger.info('ivcs-js-sdk:send asr text message error', e);
									}
								);
							}
						});
						return;
					} else {
						this.stopLocalStream();
						onFailure(new IvcsError('1017', 'there is no other user in the current session'));
						return;
					}
				} catch (e) {
					Logger.info(`ivcs-js-sdk:answer-error:${new Date().toISOString()},error:${e}`);
					this.stopLocalStream();
					onFailure(e);
					return;
				}
			});
		} catch (error) {
			Logger.info(`ivcs-js-sdk:answer-error:${new Date().toISOString()},error:${error}`);
			this.stopLocalStream();
			onFailure(error);
			return;
		}
	}

	/**
	 * 接听音视频呼叫,成功后调用startMedia，推送本地画面
	 * @param {string} mediaType 'audio'/'video'
	 * @param {function} onSuccess {sessionId,sessionData,customerSendMessage,customerClientType}
	 * @param {function} onFailure
	 */
	async answerMediaCall(mediaType = 'video', onSuccess, onFailure) {
		if (!this.ringData.url) {
			Logger.warning(`ivcs-js-sdk:no ring or ring stop,${new Date()}`);
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'no ring or ring stop');
			onFailure(error);
			return;
		}
		try {
			let sessionId = this.ringData.sessionId;
			let invitationId = this.ringData.invitationId ? this.ringData.invitationId : '';
			let accessToken = this.accessToken;
			let joinData = {
				media: mediaType,
				userData: {
					skill: '',
					specifyAgent: '',
				},
				sessionId: sessionId,
				service: this.ringData.bizScenarioId,
				clientInfo: getBrowserClientInfo(),
				invitationId: invitationId,
			};
			let answerTask = setTimeout(async () => {
				try {
					Logger.info(`ivcs-js-sdk:answer-start-answerCall:${new Date().toISOString()}`);
					// eslint-disable-next-line no-unused-vars
					let answerCallResult = await answerCall(this.amsClient);
					this.ccsClient = new CCSClient();
					this.imUpgradeMedia = new ImUpgradeMedia();
					Logger.info(`ivcs-js-sdk:answer-start-connect-ccs:${new Date().toISOString()}`);
					// eslint-disable-next-line no-unused-vars
					let connectCcsResult = await connect(this.ccsClient, this.ccsUrl, accessToken, error => {
						clearTimeout(answerTask);
					});
					ccsErrorEvent(info => {
						Logger.warning(`ivcs-js-sdk:css-error-event: ${JSON.stringify(info)}  ${new Date().toISOString()}`);
						releaseCcsClient(this.ccsClient);
						dispatchEvent(ccsErrorName, systemErrorCode.ccsSocketIoError, info);
						clearTimeout(answerTask);
					});
					let customerSendMessage = [];
					this.onTextMessage(message => {
						customerSendMessage.push(message);
					});
					Logger.info(`ivcs-js-sdk:answer-start-joinsession:${new Date().toISOString()}`);
					this.joinResult = await joinSession(this.ccsClient, joinData);
					this.ccsClient.members = this.joinResult.members;
					sessionMemberJoinQuitEvent(
						this.ccsClient,
						quitCustomer => {
							//customer hangup,the room has been removed and the CCS connection has been disconnected
							this.onCustomerQuitSession(quitCustomer);
							//customer quit session while answer not success
							if (!this.ccsClient.members) {
								onFailure(new IvcsError('1017', 'customer quit session'));
							}
							return;
						},
						joinAgent => {
							this.ccsClient.members = [...this.ccsClient.members, joinAgent];
						},
						joinCustomer => {
							this.ccsClient.members = [...this.ccsClient.members, joinCustomer];
						}
					);
					//Under normal circumstances, the number of users in the current session is greater than or equal to 2
					if (this.joinResult.members && this.joinResult.members.length > 1) {
						Logger.info(`ivcs-js-sdk-answer-join-session success:${new Date().toISOString()}`);
						this.ccsClient.onSessionIdChange(data => {
							if (data?.data?.sessionId && this.irtcClient) {
								this.irtcClient.setAllSubscribeCallQuality(deepCopy(this.irtcClient.sessionId));
								this.irtcClient.sessionId = data.data.sessionId;
								this.systemRecordId = null;
							}
						});
						this.systemRecordId = this.joinResult.recordingId;
						this.recordingIds.push(this.systemRecordId);
						onSuccess({
							sessionId: sessionId,
							sessionData: this.joinResult,
							customerSendMessage: customerSendMessage,
							customerClientType: this.joinResult.customerClientType,
						});
						this.ccsClient.members = this.joinResult.members;
					} else {
						onFailure(new IvcsError('1017', 'there is no other user in the current session'));
						return;
					}
				} catch (e) {
					Logger.info(`ivcs-js-sdk-answer-call-error:${new Date().toISOString()},error:${e}`);
					onFailure(e);
					return;
				}
			});
		} catch (error) {
			Logger.info(`ivcs-js-sdk-answer-call-catch-error:${new Date().toISOString()},error:${error}`);
			onFailure(error);
			return;
		}
	}

	/**
	 * 开启本地音视频，在接听音视频呼叫后执行，
	 * @param {string} videoLabel  视频标签
	 * @param {string} mediaType  接听的呼叫类型（默认是video）
	 * @param {object} customizedMediaStreams  定制化的mediaStream,默认情况下传undefined
	 * @param {object | null} mediaStreamConstraints  MediaStreamConstraints 对象 或者 null
	 * @param {function} onSuccess 接听成功 {publicationId,participantId,realMediaType} realMediaType="实际坐席推送的流类型，audio/video"
	 * @param {function} onFailure 接听失败 {code,message},code:'100001'||'100002' 表示不支持的浏览器
	 */
	async startMedia(videoLabel, mediaType = 'video', customizedMediaStreams, mediaStreamConstraints, onSuccess, onFailure) {
		if (isEmptyString(videoLabel)) {
			Logger.warning(`ivcs-js-sdk:The videoLabel is used to tag the current role information,videoLabel is invalid`);
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'videoLabel is a required parameter');
			onFailure(error);
			return;
		}
		if (customizedMediaStreams) {
			if (!isEffectiveMediaStreams(customizedMediaStreams)) {
				Logger.warning(`ivcs-js-sdk:customizedMediaStreams is invalid`);
				let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'customized MediaStreams is invalid');
				onFailure(error);
				return;
			}
		}
		try {
			let backendUrl = this.backendUrl;
			let turnServerUrl = this.associatedUrls.turnserverUrl;
			let turnServerCredential = this.associatedUrls.turnserverCredential;
			let turnServerUsername = this.associatedUrls.turnserverUsername;
			Logger.info(`ivcs-js-sdk-answer-start-create-stream:${new Date().toISOString()}`);
			this.irtcClient = new IRtcClient(turnServerUrl, turnServerCredential, turnServerUsername);
			this.irtcClient.clientInfo.clientId = this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '';
			this.irtcClient.clientInfo.userName = this.agentName;
			this.irtcClient.clientInfo.role = 'agent';
			this.irtcClient.sessionId = deepCopy(this.sessionId);
			this.irtcClient.accessToken = deepCopy(this.accessToken);
			this.irtcClient.backendUrl = deepCopy(this.backendUrl);
			Logger.info(`ivcs-js-sdk-answer-start-join-room:${new Date().toISOString()}`);
			this.roomId = this.joinResult.roomId;
			this.roomToken = this.joinResult.roomToken;
			this.roomInfo = await joinRoom(this.irtcClient, this.roomId, this.roomToken);
			zmsAddEventListener(this.irtcClient);
			let haveAudio = true;
			this.sendStartVideoProgressMessage(videoLabel, 'agent', 'permission');
			let haveVideo = mediaType === 'video' ? true : false;
			if (mediaType === 'video') {
				let haveVideoDevice = await TestVideoDevice();
				haveVideo = haveVideoDevice ? true : false;
			}
			let streamInfo = {
				participantId: this.participantId ? this.participantId : '',
				clientId: this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '',
			};
			let userInfo = {
				clientId: this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '',
				userName: this.agentName ? this.agentName : '',
				role: 'agent',
			};
			let streamOptions = {
				audioConstraints: validateLocalStreamParameter(mediaStreamConstraints) === 'ok' ? mediaStreamConstraints.audioConstraints : this.mediaOptions.cameraStreamAudioConstraints,
				videoConstraints: validateLocalStreamParameter(mediaStreamConstraints) === 'ok' ? mediaStreamConstraints.videoConstraints : this.mediaOptions.cameraStreamVideoConstraints,
				cameraPublishOptions: validateLocalStreamParameter(mediaStreamConstraints) === 'ok' ? mediaStreamConstraints.publishOptions : this.mediaOptions.cameraPublishOptions,
			};
			this.customizedMediaStreamsOptions = streamOptions;
			console.log('ivcs-js-sdk-validate LocalStreamParameter:', mediaStreamConstraints, validateLocalStreamParameter(mediaStreamConstraints));
			this.localStream = customizedMediaStreams ? customizedMediaStreams : await createCameraMicStream(this.irtcClient, streamOptions, videoLabel, haveAudio, haveVideo, streamInfo, userInfo);
			Logger.info(`ivcs-js-sdk-answer-start-publish-stream:${new Date().toISOString()}`);
			this.sendStartVideoProgressMessage(videoLabel, 'agent', 'publish');
			let publication = await publishStream(this.irtcClient, this.localStream, streamOptions.cameraPublishOptions);
			publicationAddEventListener(this.irtcClient, publication);
			this.ccsClient.sendZmsRoomEvent({ event: 'publish-success' }, (s, r, m) => {
				Logger.info(`ivcs-js-sdk:send zms-room-event publish-success event with status:${s},response:${r},msgId:${m},at:${new Date().toISOString()}`);
			});
			this.ccsClient && this.ccsClient.noticeStreamId({ streamId: publication.id });
			this.publication = publication;
			let participantId = this.roomInfo.self.id;
			this.participantId = participantId;
			Logger.info(`ivcs-js-sdk-answer-start-reasy-to-talk:${new Date().toISOString()}`);
			// eslint-disable-next-line no-unused-vars
			let readyToTalkResult = await readyToTalk(participantId, {}, 'customers', this.ccsClient);
			this.joinResult.customerClientType = getCustomerClientInfoWithMembers(this.joinResult.members);
			if (this.joinResult.customerClientType == this.miniType) {
				Logger.info(`ivcs-js-sdk-answer-start-createTokenWithRtmpServer:${new Date().toISOString()}`);
				let rtmpToken = await createTokenWithRtmpServer(backendUrl, this.accessToken, 60);
				let streamOutsUrl = this.associatedUrls.rtmpServer + '/' + this.joinResult.clientId + this.joinResult.sessionId + '?token=' + rtmpToken;
				// 小程序纯音频模式下，video设为false
				const videoId = mediaType != 'video' ? null : publication.id;
				Logger.info(`ivcs-js-sdk-answer-start-streamOutsToMediaServer:${new Date().toISOString()}`);
				let streamOuts = await streamOutsToMediaServer(backendUrl, this.roomId, publication.id, videoId, streamOutsUrl);
				// 缓存数据用于纯语音推流转换到音视频推流
				this.joinResult.callMediaType = mediaType;
				this.joinResult.audioId = publication.id;
				this.joinResult.videoId = publication.id;
				this.joinResult.streamOutsUrl = streamOutsUrl;
				this.joinResult.streamingOutsId = streamOuts.data.id;
			}
			Logger.info(`ivcs-js-sdk-answer-start-addStreamToMixStream:${new Date().toISOString()}`);
			// eslint-disable-next-line no-unused-vars
			let addStreamToMix = await addStreamToMixStream(backendUrl, this.roomId, publication.id, this.accessToken);
			// eslint-disable-next-line no-unused-vars
			let mixStreamId = this.irtcClient.mixStreamId;
			this.localPublicationId = publication.id;
			onSuccess({
				publicationId: publication.id,
				participantId: this.roomInfo.self.id,
				realMediaType: haveVideo ? 'video' : 'audio',
			});

			Logger.info(`ivcs-js-sdk-answer-startmedia-success:${new Date().toISOString()}`);
			/**临时解决方案，修复将当前坐席流加入mix流后，并未立即生效，导致无法将坐席工号贴到mix流中。加入1秒延时，确保坐席流成功加入到mix流中 */
			setTimeout(() => {
				Logger.info(`ivcs-js-sdk:notify-stream-add-agentName:${new Date().toISOString()}`);
				addAgentNameInRecordFile(backendUrl, this.sessionId, this.localPublicationId, this.agentName, this.accessToken);
			}, 1000);
			//warning:recordings of mix streams need to be placed in the background
			this.streamAddListener = addEventListener(IRTC_CUSTOMEREVENT.STREAMADD, info => {
				Logger.info('ivcs-js-sdk:new stream add with info:', info, ' at:', new Date());
				if (info && info.data) {
					Logger.info('ivcs-js-sdk:new stream add with stream:', info.data, ' at:', new Date());
					//添加asr的监听
					startAsrEvent(this.backendUrl, this.roomId, info.data.id, this.accessToken, participantId, 'baidu');
				}
			});
			this.streamAsrListener = addEventListener(IRTC_CUSTOMEREVENT.ASR, info => {
				Logger.info('ivcs-js-sdk:new stream asr event:', info, ' at:', new Date());
				if (info && info.data) {
					Logger.info('ivcs-js-sdk:new stream asr event:', info.data, ' at:', new Date());
					this.sendTextMessageWithFromClientId(
						info.data.content,
						info.data.clientId,
						'all',
						s => {
							Logger.info('ivcs-js-sdk:send asr text message success', s);
						},
						e => {
							Logger.info('ivcs-js-sdk:send asr text message error', e);
						}
					);
				}
			});
			return;
		} catch (error) {
			Logger.info(`ivcs-js-sdk-answer-startmedia-catch-error:${new Date().toISOString()},error:${error}`);
			this.stopLocalStream();
			onFailure(error);
			return;
		}
	}

	/**
	 * 释放本地流,fix 坐席再创建流的时候，会话异常，导致创建成功后，无法释放
	 * @ignore
	 */
	stopLocalStream() {
		if (this.localStream) {
			stopStream(this.localStream);
			this.localStream = null;
		}
		setTimeout(() => {
			if (this.localStream) {
				stopStream(this.localStream);
				this.localStream = null;
			}
		}, 1000);
		setTimeout(() => {
			if (this.localStream) {
				stopStream(this.localStream);
				this.localStream = null;
			}
		}, 2000);
		setTimeout(() => {
			if (this.localStream) {
				stopStream(this.localStream);
				this.localStream = null;
			}
		}, 3000);
	}

	/**
	 * 告知访客坐席流已准备好
	 * @param {string} participantId 发布流用户唯一ID
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.sendReadyToTalk(
				participantId,
			resp => {
				console.log('sendReadyToTalk success', resp);
			},
			error => {
				console.log('sendReadyToTalk error', error);
			}
		);
	 */
	async sendReadyToTalk(participantId, onSuccess, onError) {
		try {
			let result = await readyToTalk(participantId, {}, 'customers', this.ccsClient);
			onSuccess(result);
		} catch (error) {
			onError(error);
		}
	}

	/**
	 * 语音升级到视频通话
	 * @param {function} onSuccess 成功 MediaStream
	 * @param {function} onFailure 失败 {1022:未开启音视频通话,1023:updateMedia升级失败}
	 * @param {object} customStream 自定义视频流,默认为空。
	 * @example
	 * ivcs.agent.voiceToVideo(
			resp => {
				console.log('voice to video success', resp);
			},
			error => {
				console.log('voice to video error', error);
			}
		);
	 */
	async voiceToVideo(onSuccess, onFailure, customStream = undefined, videoId = undefined, audioInputDeviceID = undefined, videoInputDeviceID = undefined) {
		if (!this.publication) {
			Logger.warning(`ivcs-js-sdk:no audio or video calls`);
			let error = new IvcsError('1022', 'No audio or video calls');
			onFailure(error);
			return;
		}
		let constraints = {
			audio: audioInputDeviceID ? { ...this.mediaOptions.cameraStreamAudioConstraints, deviceId: audioInputDeviceID } : this.mediaOptions.cameraStreamAudioConstraints,
			video: videoInputDeviceID ? { ...this.mediaOptions.cameraStreamVideoConstraints, deviceId: videoInputDeviceID } : this.mediaOptions.cameraStreamVideoConstraints,
		};
		Logger.info('ivcs-js-sdk:voiceToVideo constraints:', constraints);
		try {
			let stream = customStream ? customStream : await this.irtcClient.createAudioMediaStream(constraints, {}, false);
			this.publication
				.updateMedia(stream)
				.then(resp => {
					Logger.info('ivcs-js-sdk:voice to video success', resp);
					if (videoId) {
						const isMeStream = true;
						renderStreamWithVideoLabel(
							stream,
							videoId,
							isMeStream,
							() => {},
							() => {}
						)
							.then()
							.catch(error => {
								Logger.info(`ivcs-js-sdk:voice to video renderStreamWithVideoLabel label,${videoId} error:${error}',${new Date().toISOString()}`);
							});
					}
					onSuccess(stream);
				})
				.catch(error => {
					Logger.error('ivcs-js-sdk:voice to video error', error);
					let err = new IvcsError('1023', error.message || error.name || error);
					onFailure(err);
				});
		} catch (error) {
			Logger.error('ivcs-js-sdk:voice to video crash error', error);
			let err = new IvcsError(error.code || '1023', error.message || error.name || error);
			onFailure(err);
		}
	}

	/**
	 * 视频降级成音频通话
	 * @param {function} onSuccess 成功 MediaStream
	 * @param {function} onFailure 失败 {1022:未开启音视频通话,1023:updateMedia升级失败}
	 * @param {string | undefined} 音频设备ID
	 * @param {string} videoId 坐席自己画面video标签的id，默认为空，不传则需要坐席自己去拿到mediaStream去渲染,支持传多个标签，格式'videoId1,videoId2'
	 * @example
	 * ivcs.agent.videoToVoice(
				resp => {
					console.log('videoToVoice success', resp);
				},
				error => {
					console.log('videoToVoice error', error);
				},
				audioInputDeviceID
			);
	 */
	async videoToVoice(onSuccess, onFailure, audioInputDeviceID = undefined, videoId) {
		if (!this.publication) {
			Logger.warning(`ivcs-js-sdk:no audio or video calls`);
			let error = new IvcsError('1022', 'No audio or video calls');
			onFailure(error);
			return;
		}
		let constraints = {
			audio: audioInputDeviceID ? audioInputDeviceID : this.mediaOptions.cameraStreamAudioConstraints,
			video: false,
		};
		Logger.info('ivcs-js-sdk:videoToVoice constraints:', constraints);
		try {
			let stream = await this.irtcClient.createVideoMediaStream(constraints, {}, false);
			this.publication
				.updateMedia(stream)
				.then(resp => {
					Logger.info('ivcs-js-sdk:video to voice success', resp);
					onSuccess(stream);
				})
				.catch(error => {
					Logger.error('ivcs-js-sdk:video to voice error', error);
					let err = new IvcsError('1023', error.message || error.name || error);
					onFailure(err);
				});
		} catch (error) {
			Logger.error('ivcs-js-sdk:video to voice crash error', error);
			let err = new IvcsError(error.code || '1023', error.message || error.name || error);
			onFailure(err);
		}
	}

	/**
	 * 监听tts辅助播报语音开始播报
	 * @param {function} ack {streamId:string(tts语音流id),streamLabel:string(tts语音流标签),userInfo:object(开始tts播报的坐席个人信息),content:string(播报的文本内容)}
	 * @example
	 * ivcs.client.onTTSStreamSpeaking((info)=>{});
	 */
	onTTSStreamSpeaking(ack) {
		this.addEventListener(IRTC_CUSTOMEREVENT.ttsStreamSpeaking, result => {
			const data = result.data;
			ack(data);
		});
	}

	/**
	 * 监听tts辅助播报语音播报结束
	 * @param {function} ack {streamId:string(tts语音流id),streamLabel:string(tts语音流标签),userInfo:object(开始tts播报的坐席个人信息),content:string('ok')}
	 * @example
	 * ivcs.client.onTTSStreamSpeakDone((info)=>{});
	 */
	onTTSStreamSpeakDone(ack) {
		this.ttsStreamSpeakDones = new Map();
		this.addEventListener(IRTC_CUSTOMEREVENT.ttsStreamSpeakDone, result => {
			let data = deepCopy(result).data;
			if (this.ttsStreamSpeakDones.size === 0) {
				this.ttsStreamSpeakDones.set(data.streamId, data);
				ack(data);
				return;
			}
			let exist = false;
			for (let [key, val] of this.ttsStreamSpeakDones.entries()) {
				if (key === data.streamId) {
					Logger.info('ivcs-js-sdk:ttsStreamSpeakDone already happen', data);
					exist = true;
					return;
				}
			}
			if (!exist) {
				this.ttsStreamSpeakDones.set(data.streamId, data);
				ack(data);
			}
		});
	}

	/**
	 * 监听通话中那个流正在说话
	 * @param {function} ack {streamId:string(流id),streamLabel:string(流标签),userInfo:object(), audioOnly:boolean()}
	 * @example
	 * ivcs.agent.onActiveAudioInputStream((info)=>{});
	 */
	onActiveAudioInputStream(ack) {
		this.addEventListener(IRTC_CUSTOMEREVENT.activeAudioInputStream, result => {
			const data = result.data;
			ack(data);
		});
	}

	/**
	 * 监听会话录像中断事件。
	 * @param {function} ack {recordingId: string(录制id)}
	 * @ignore
	 * @example
	 * ivcs.agent.onSessionRecordCrash((data)=>{console.log(data.recordingId)});
	 */
	onSessionRecordCrash(ack) {
		this.addEventListener(IRTC_CUSTOMEREVENT.sessionOnEnd, result => {
			const data = result.data;
			Logger.info(`ivcs-js-sdk:zms sessionOnEnd, data: ${JSON.stringify(data)},systemRecordId:${this.systemRecordId},time:${new Date().toISOString()}`);
			if (data && data.sessionId === this.systemRecordId && this.irtcClient) {
				// 会话录像异常上报到服务端。
				this.irtcClient.updateRecordCrashEvent(this.systemRecordId);
			}
			if (this.systemRecordId) {
				// 会话进行中，告知应用层，一些相关的录制中断事件，如录屏/区域录屏
				if (data && data.sessionId && this.recordingIds.includes(data.sessionId)) {
					ack({ recordingId: data.sessionId });
				}
			}
		});
		this.addEventListener(IRTC_CUSTOMEREVENT.sessionOnError, result => {
			const data = result.data;
			Logger.info(`ivcs-js-sdk:zms sessionOnError, data: ${JSON.stringify(data)},systemRecordId:${this.systemRecordId},time:${new Date().toISOString()}`);
			if (data && data.sessionId === this.systemRecordId && this.irtcClient) {
				// 会话录像异常上报到服务端。
				this.irtcClient.updateRecordCrashEvent(this.systemRecordId);
			}
			if (this.systemRecordId) {
				// 会话进行中，告知应用层，一些相关的录制中断事件，如录屏/区域录屏
				if (data && data.sessionId && this.recordingIds.includes(data.sessionId)) {
					ack({ recordingId: data.sessionId });
				}
			}
		});
	}

	/**
	 * 监听访客端本地推送画面的媒体连接状态。
	 * @param {function} ack {streamLabel:string(本地推流标签),userInfo:object(该画面包含的client信息),publishStatus:{status:string('failed'|'connecting'|'connected'|'disconnected'|'quality-report'),data:object(网络状况信息)}}
	 * @example
	 * ivcs.agent.onPublishMediaStatus((info)=>{});
	 */
	onPublishMediaStatus(ack) {
		this.addEventListener(IRTC_CUSTOMEREVENT.publishStreamStatus, result => {
			const data = result.data;
			ack(data);
		});
	}

	/**
	 * 监听渲染的远端画面的媒体连接状态。
	 * @param {function} ack {streamLabel:string(渲染的流标签),userInfo:object(该画面包含的client信息),renderStatus:{status:string('failed'|'connecting'|'connected'|'disconnected'|'quality-report'),data:object(网络状况信息)}}
	 * @example
	 * ivcs.agent.onPublishMediaStatus((info)=>{});
	 */
	onRenderMediaStatus(ack) {
		this.addEventListener(IRTC_CUSTOMEREVENT.remoteStreamStatus, result => {
			const data = result.data;
			ack(data);
		});
	}

	/**
	 * 拒绝接听
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败 code:['3001','3006','3008','3009']
	 * @example
	 * ivcs.agent.reject(
			function(resp) {
				console.log('reject success', resp);
			},
			function(error) {
				console.log('reject error', error);
			}
		);
	 */
	reject(onSuccess, onFailure) {
		operateEvent(this.amsClient, 'rejectCall', onSuccess, onFailure);
	}

	/**
	 * 挂断会话
	 * @param {object} reason 挂断原因 如:{reason:string(), type:string()},默认传值为{}, 转接挂断{type:'' || 'transfer' || 'multiple'}
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * let hangUpReason = {};
		ivcs.agent.hangup(
			hangUpReason,
			resp => {
				console.log('hangUp success', resp);
			},
			error => {
				console.log('hangUp error', error);
			}
		);
	 */
	async hangup(reason = {}, onSuccess, onFailure) {
		this.systemRecordId = null; // 挂断设置空录制id，保证监听到录制异常不需要上报给应用层
		this.recordingIds = [];
		this.irtcWhiteBoard = null;
		this.irtcClient ? zmsRemoveEventListener(this.irtcClient) : undefined;
		this.stopMonitorNetwork();
		let result = null;
		try {
			this.stopLocalStream();
			//the agent who is ringing need hangupCall && the agent who is joinSession need't hangupCall
			if (this.irtcClient && this.roomInfo) {
				leaveRoom(this.irtcClient);
				this.irtcClient = null;
				this.roomInfo = null;
			}
			if (this.ccsClient && this.ccsClient.accessToken) {
				quitSession(reason, this.ccsClient);
			}
			if (this.amsClient && this.amsClient.accessToken && this.ringData && this.ringData.sessionId) {
				result = await hangupCall(this.amsClient, reason);
			}
			//remove event-listener
			onSuccess({ status: 'ok', sessionId: this.sessionId, result });
		} catch (e) {
			Logger.info('ivcs-js-sdk:agent hangup error', e, new Date().toISOString());
			onFailure(e);
		} finally {
			if (this.streamAddListener) {
				removeEventListener(IRTC_CUSTOMEREVENT.STREAMADD, this.streamAddListener);
			}
			if (this.streamAsrListener) {
				removeEventListener(IRTC_CUSTOMEREVENT.ASR, this.streamAsrListener);
			}
			this.eventList &&
				this.eventList.forEach((value, key) => {
					if (key != systemErrorName) {
						this.removeEventListener(key, value);
					}
				});
			this.ringData = {};
		}
	}

	/**
	 * 添加事件监听
	 * @param {string} eventName  事件名，目前系统支持的事件有:onCustomerQuitSession,onAgentQuitSession,onAgentJoinSession,systemError,onStreamAdd,onStreamEnd,ccsError
	 * @param {function} ack 事件返回具体的数据
	 * @returns {function} 返回事件函数
	 * @example
	 * // 监听通话中 有流加入
	 *  this.streamAddEvent = ivcs.agent.addEventListener('onStreamAdd', streamInfo => {
				console.log('onStreamAdd', streamInfo);
				let label = streamInfo.data.streamLabel; // 流标签，可根据label去订阅这个流画面
				let streamId = streamInfo.data.id; // 流原始id
				let userInfo = streamInfo.data.userInfo;// 流发布者的用户信息，{clientId,userName}
				let audioOnly = streamInfo.data.audioOnly;// 是否是纯语音流
				let isAudioMute = streamInfo.data.isAudioMute; // 流是否是静音 <发布者静音则为true>
				let isVideoMute = streamInfo.data.isVideoMute; // 流视频是否是停止的 <发布者暂停推送视频画面则为true>

		});
		// 监听通话中 有流离开
		this.streamEndEvent = ivcs.agent.addEventListener('onStreamEnd', streamInfo => {
				console.log('onStreamEnd', streamInfo);
				let label = streamInfo.data.streamLabel; // 流标签，可根据label去隐藏这个流画面
		});
		// 监听流 语音，视频发生变化。
		this.streamUpdateEvent = ivcs.agent.addEventListener('onStreamUpdated', streamInfo => {
				console.log('onStreamUpdated', streamInfo);
				let label = streamInfo.data.streamLabel; // 流标签
				let streamId = streamInfo.data.id; // 流原始id
				let userInfo = streamInfo.data.userInfo;// 流发布者的用户信息，{clientId,userName}
				// 流更新信息
				// updateInfo:[{field:'media.video.status',value:true}] 视频恢复正常状态,
				// updateInfo:[{field:'media.video.status',value:false}] 视频关闭
				// updateInfo:[{field:'media.audio.status',value:true}] 声音恢复正常状态,
				// updateInfo:[{field:'media.audio.status',value:false}] 声音关闭
				let updateInfo = streamInfo.data.info; // 流更新信息，数组类型
		});
	 */
	addEventListener(eventName, ack) {
		Logger.info(`ivcs-js-sdk:add event listener with Name,${eventName}`);
		let event_function = addEventListener(eventName, ack);
		this.eventList.set(eventName, event_function);
		return event_function;
	}

	/**
	 * 移除事件监听
	 * @param {string} eventName 事件名
	 * @param {function} eventImplementationFunction  添加事件监听对应的返回的事件函数
	 * @example
	 * ivcs.agent.removeEventListener('onStreamEnd', this.streamEndEvent);
	 */
	removeEventListener(eventName, eventImplementationFunction) {
		removeEventListener(eventName, eventImplementationFunction);
	}

	/**
	 * 渲染视频流
	 * @param {string} streamLabel 需要渲染的视频标签
	 * @param {string} videoId 被渲染的video标签的id
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @param {function} onPlayError video播放视频失败,需要用户交互触发播放,如:let video = document.getElementById(videoId);video && video.play();
	 * @param {function} onEnded 画面的视频帧结束,返回video标签的id，业务可根据video标签id，给到不同的处理逻辑，自己的画面标签id,可通过切换摄像头恢复，对方的画面标签id,可重新订阅(renderStream)恢复
	 * @example
	 * this.streamAddEvent = ivcs.agent.addEventListener('onStreamAdd', streamInfo => {
		  let label = streamInfo.data.streamLabel;
		});
		ivcs.agent.renderStream(
						label,//需要渲染的视频标签
						label,//被渲染的video标签的id
						resp => {
							console.log('render success', resp, label, label);
						},
						error => {
							console.log('render error', error);
						},
						playError => {
							console.log('render play error', playError);
							this.userGesturePlayVideo(label);
						},
						onEnded => {
							console.log('render onEnded', onEnded);
						}
					);
	 */
	async renderStream(streamLabel, videoId, onSuccess, onFailure, onPlayError, onEnded, subscribeSuccess) {
		renderRemoteStream(
			this.irtcClient,
			streamLabel,
			videoId,
			resp => {
				onSuccess(resp);
			},
			error => {
				onFailure(error);
			},
			playError => {
				onPlayError(playError);
			},
			label => {
				onEnded(label);
			},
			subscribeSuccess
		);
	}

	/**
	 * 停止渲染,主要用于订阅机器人后，机器人挂断后，取消订阅机器人。
	 * @since 1.0.4版本新增API
	 * @param {string} subscriptionId 渲染视频流成功后返回的id
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.stopRenderStream(
			subscriptionId,
			s => {
				console.log('stop render stream success', subscriptionId, s);
			},
			e => {
				console.log('stop render stream error', subscriptionId, e);
			}
		);
	 */
	stopRenderStream(subscriptionId, onSuccess, onFailure) {
		if (!this.irtcClient) {
			onFailure(new IvcsError('1024', 'irtcClient is invalid'));
			return;
		}
		let subscribes = this.irtcClient.subscriptionArr ? this.irtcClient.subscriptionArr : [];
		for (let i = 0; i < subscribes.length; i++) {
			let subscription = subscribes[i];
			if (subscriptionId === subscription.id) {
				subscription.stop();
				onSuccess('ok');
				return;
			}
		}
		onFailure(new IvcsError('1024', `subscriptionId, ${subscriptionId} is invalid`));
	}

	/**
	 * 开启屏幕共享
	 * @since 1.0.5版本更新API
	 * @param {function} onSuccess 成功 {string(publicationId)}
	 * @param {function} onFailure 失败
	 * @param {function} onStop 系统窗口被关闭
	 * @param {string} screenLabel 屏幕共享的流标签，目前可用于业务系统在多坐席的场景下区分那个坐席共享的屏幕。建议命名规范:坐席名+'screen'
	 * @param {string} to 屏幕共享给的用户的clientId,'all'所有人,'others'除了自己的其他人,'agents'所有的坐席,'customers'所有的访客
	 * @example
	 * ivcs.agent.screenShare(
			resp => {
				console.log('screen share success', resp);
			},
			error => {
				console.log('screen share error', error);
			},
			onend => {
				console.log('screen share end', onend);
			},
			'001screen',
			'customers'
		);
	 */
	async screenShare(onSuccess, onFailure, onStop, screenLabel = 'screen', to = 'customers') {
		try {
			let streamOptions = {
				audioConstraints: this.mediaOptions.screenStreamAudioConstraints,
				videoConstraints: this.mediaOptions.screenStreamVideoConstraints,
			};
			let userInfo = {
				from: {
					participantId: this.participantId ? this.participantId : '',
					clientId: this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '',
					userName: this.agentName ? this.agentName : '',
				},
				to: to,
			};
			let label = deepCopy(screenLabel);
			let screenStream = await createScreenStream(this.irtcClient, streamOptions, label, userInfo);
			this.screenShareStream = screenStream;
			// fix stop screen stream error
			this.irtcClient.createStreams.push(screenStream);
			let publishOptions = deepCopy(this.mediaOptions.screenPublishOptions);
			screenStream.mediaStream.getAudioTracks().length > 0 ? (publishOptions.audio ? 1 : (publishOptions.audio = deepCopy(this.mediaOptions.screenPublishOptions.audio))) : (publishOptions.audio = false);
			screenStream.mediaStream.getVideoTracks().length > 0 ? (publishOptions.video ? 1 : (publishOptions.video = deepCopy(this.mediaOptions.screenPublishOptions.video))) : (publishOptions.video = false);
			let publication = await publishStream(this.irtcClient, screenStream, publishOptions);
			if (this.joinResult.customerClientType == this.miniType) {
				let backendUrl = this.backendUrl;
				let rtmpToken = await createTokenWithRtmpServer(backendUrl, this.accessToken, 60);
				let streamOutsUrl = this.associatedUrls.rtmpServer + '/screen' + this.joinResult.sessionId + '?token=' + rtmpToken;
				let audioId = screenStream.mediaStream.getAudioTracks().length > 0 ? publication.id : null;
				let videoId = screenStream.mediaStream.getVideoTracks().length > 0 ? publication.id : null;
				// eslint-disable-next-line no-unused-vars
				let streamOuts = await streamOutsToMediaServer(backendUrl, this.roomId, audioId, videoId, streamOutsUrl);
			}
			screenStream.mediaStream.getVideoTracks()[0].onended = () => {
				this.stopScreenShare(
					publication.id,
					success => {
						Logger.info('ivcs-js-sdk:screen shared popup has been closed', success);
					},
					error => {
						Logger.info('ivcs-js-sdk:screen shared popup has been closed', error);
					}
				);
				onStop();
			};
			// 保存屏幕共享的操作
			// this.ccsClient && this.ccsClient.screenShare({},to,(status)=>{
			// 	Logger.info('save screenshare operate status,',status);
			// });
			onSuccess(publication.id);
		} catch (error) {
			Logger.info('ivcs-js-sdk:screen share error', error);
			if (this.screenShareStream) {
				stopStream(this.screenShareStream);
			}
			onFailure(error);
		}
	}

	/**
	 * 保存对某一个访客开启屏幕共享，用于业务系统留痕
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人;
	 * @example
	 * const customerClientId = '12312313123';
	 * ivcs.agent.saveScreenShareRecord(customerClientId);
	 */
	saveScreenShareRecord(toWhom) {
		this.ccsClient &&
			this.ccsClient.screenShare({}, toWhom, status => {
				Logger.info('ivcs-js-sdk:save screenShare operate status,', status);
			});
	}

	/**
	 * 停止屏幕共享
	 * @since 1.0.5版本更新API
	 * @param {string} publicationId 开启屏幕共享成功后返回的publicationId
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.stopScreenShare(
			this.screenPublicationId,
			resp => {
				console.log('stop screenshare success', resp);
				
			},
			error => {
				console.log('stop screenshare error', error);
			}
		);
	 */
	async stopScreenShare(publicationId, onSuccess, onFailure) {
		let flag = false;
		if (this.irtcClient) {
			for (let i = 0; i < this.irtcClient.publicationArr.length; i++) {
				let publication = this.irtcClient.publicationArr[i];
				if (publication.id === publicationId) {
					flag = true;
					stopPublish(publication);
					for (let j = 0; j < this.irtcClient.createStreams.length; j++) {
						let stream = this.irtcClient.createStreams[j];
						if (stream.id === publication.originId) {
							stopStream(stream);
							this.irtcClient.createStreams.splice(j, 1);
						}
					}
				}
			}
			if (flag) {
				onSuccess();
			} else {
				onFailure(new IvcsError('1010', 'publicationId is invalid'));
			}
		} else {
			onFailure(new IvcsError('1011', 'no video session'));
		}
	}

	/**
	 * 保存对某一个访客停止屏幕共享，用于业务系统留痕
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人;
	 * @example
	 * const customerClientId = '12312313123';
	 * ivcs.agent.saveStopScreenShareRecord(customerClientId);
	 */
	saveStopScreenShareRecord(toWhom) {
		this.ccsClient &&
			this.ccsClient.stopScreenShare({}, toWhom, status => {
				Logger.info('ivcs-js-sdk:save stopScreenShare operate status,', status);
			});
	}

	/**
	 * 开启系统录制
	 * @param {function} onSuccess {recordId:string(录制id)}
	 * @param {function} onFailure {code,message} code:'404'='不在room中','405'='不在session中','406'='通知其他人开启录制失败'
	 */
	async startSystemRecord(onSuccess, onFailure) {
		try {
			if (!this.roomId) {
				onFailure(new IvcsError('404', 'have no room session'));
				return;
			}
			if (!this.ccsClient) {
				onFailure(new IvcsError('405', 'not in session'));
				return;
			}
			let audioId = this.roomId + '-common';
			let videoId = this.roomId + '-common';
			let type = 'system';
			let backendUrl = this.backendUrl;
			let accessToken = this.amsClient.accessToken;
			let recordId = await recordStream(backendUrl, this.roomId, audioId, videoId, accessToken, false, this.sessionId, type);
			this.systemRecordId = recordId;
			this.recordingIds.push(recordId);
			this.ccsClient.startRecording({ type: 'system', status: 'start', recordId: recordId }, 'others', (s, r, m) => {
				Logger.info(`ivcs-js-sdk:send start-system-record event with status:${s},response:${r},msgId:${m},at:${new Date().toISOString()}`);
				if (s !== 'ok') {
					onFailure(new IvcsError('406', 'notice others start-system-record error'));
				}
			});
			onSuccess({ recordId: recordId });
		} catch (error) {
			onFailure(error);
		}
	}

	/**
	 * 停止系统录制
	 * @param {string} recordId
	 * @param {function} onSuccess 'ok'
	 * @param {function} onFailure  {code,message} code:'404'='不在room中','405'='不在session中','406'='通知其他人停止录制失败', '407'='recordId为空'
	 */
	async stopSystemRecord(recordId, onSuccess, onFailure) {
		try {
			if (!this.roomId) {
				onFailure(new IvcsError('404', 'have no room session'));
				return;
			}
			if (!this.ccsClient) {
				onFailure(new IvcsError('405', 'not in session'));
				return;
			}
			if (!recordId) {
				onFailure(new IvcsError('407', 'recordId is empty'));
				return;
			}
			let accessToken = this.amsClient.accessToken;
			let backendUrl = this.backendUrl;
			this.systemRecordId = 'stop';
			this.recordingIds = this.recordingIds.filter(item => item !== recordId);
			this.ccsClient.stopRecording({ type: 'system', status: 'stop', recordId: recordId }, 'others', (s, r, m) => {
				Logger.info(`ivcs-js-sdk:send stop-system-record event with status:${s},response:${r},msgId:${m},at:${new Date().toISOString()}`);
				if (s !== 'ok') {
					onFailure(new IvcsError('406', 'notice others stop-system-record error'));
				}
			});
			await stopRecordStream(backendUrl, this.roomId, recordId, accessToken, false, this.sessionId);
			onSuccess('ok');
		} catch (error) {
			onFailure(error);
			Logger.error('ivcs-js-sdk:stop system record error', error, new Date().toISOString());
			this.systemRecordId = recordId; // 停止录制失败，继续缓存录制id
			this.recordingIds.push(recordId); // 缓存录制id
			this.ccsClient.startRecording({ type: 'system', status: 'start', recordId: recordId }, 'others', (s, r, m) => {
				Logger.info(`ivcs-js-sdk:stopSystemRecord crash,resend start-system-record event with status:${s},response:${r},msgId:${m},at:${new Date().toISOString()}`);
			});
		}
	}

	/**
	 * 监听系统录像变化事件
	 * @param {function} ack {event:'system-record',status:'start'|'stop',recordId:'', result:'success'|'error'}
	 */
	onSystemRecordUpdate(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onSystemRecordUpdate error', isValid);
			return;
		}
		this.ccsClient.onSystemRecordEvent(data => {
			if (data && data.event === 'system-record' && data.status === 'start') {
				this.systemRecordId = data.recordId;
				this.recordingIds.push(data.recordId);
				ack(data);
			} else if (data && data.event === 'system-record' && data.status === 'stop') {
				// 停止录像，设置系统录制停止，防止错误上报录像异常事件
				this.systemRecordId = 'stop';
				this.recordingIds = this.recordingIds.filter(item => item !== data.recordId);
				ack(data);
			}
		});
	}

	/**
	 * 开启录制屏幕，借助(chrome)屏幕共享功能,录制特定的窗口
	 * @since 1.0.5版本更新API
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @param {function} onStop 系统窗口被关闭（录制结束）
	 * @param {function} onRecordError 录制失败
	 * @example
	 * ivcs.agent.startRecordScreen(
				resp => {
					this.screenRecordId = resp.recordId;
					this.screenRecordPublicationId = resp.publicationId;
				},
				error => {
					console.log('record screen error', error);
				},
				stop => {
					console.log('record screen stop', stop);
				}
		);
	 */
	async startRecordScreen(onSuccess, onFailure, onStop, onRecordError) {
		try {
			let streamOptions = {
				audioConstraints: deepCopy(this.mediaOptions.screenStreamAudioConstraints),
				videoConstraints: deepCopy(this.mediaOptions.screenStreamVideoConstraints),
			};
			const recordScreenStreamLabel = 'recordScreen';
			const userInfo = {
				clientId: this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '',
				userName: this.agentName || '',
			};
			let screenStream = await createScreenStream(this.irtcClient, streamOptions, recordScreenStreamLabel, userInfo);
			this.recordScreenStream = screenStream;
			// fix stop screen stream error
			this.irtcClient.createStreams.push(screenStream);
			let publishOptions = deepCopy(this.mediaOptions.screenPublishOptions);
			screenStream.mediaStream.getAudioTracks().length > 0 ? (publishOptions.audio ? 1 : (publishOptions.audio = deepCopy(this.mediaOptions.screenPublishOptions.audio))) : (publishOptions.audio = false);
			screenStream.mediaStream.getVideoTracks().length > 0 ? (publishOptions.video ? 1 : (publishOptions.video = deepCopy(this.mediaOptions.screenPublishOptions.video))) : (publishOptions.video = false);
			let publication = await publishStream(this.irtcClient, screenStream, publishOptions);
			let accessToken = this.amsClient.accessToken;
			let backendUrl = this.backendUrl;
			let audioId = screenStream.mediaStream.getAudioTracks() && screenStream.mediaStream.getAudioTracks().length > 0 ? publication.id : null;
			let videoId = screenStream.mediaStream.getVideoTracks() && screenStream.mediaStream.getVideoTracks().length > 0 ? publication.id : null;
			let needThrownException = true;
			let recordId; // 录制id
			try {
				recordId = await recordStream(backendUrl, this.roomId, audioId, videoId, accessToken, needThrownException, this.sessionId);
			} catch (error) {
				onRecordError && onRecordError(error);
				onFailure(error);
				this.stopScreenShare(
					publication.id,
					resp => {
						Logger.info('ivcs-js-sdk:stop screen share success with record error', resp, new Date().toISOString());
					},
					error => {
						Logger.info('ivcs-js-sdk:stop screen share error with record error', error, new Date().toISOString());
					}
				);
				return;
			}
			screenStream.mediaStream.getVideoTracks()[0].onended = async () => {
				this.stopRecordScreen(
					publication.id,
					recordId,
					success => {
						Logger.warning('ivcs-js-sdk:screen record shared popup has been closed', success);
						recordId = null;
					},
					error => {
						Logger.warning('ivcs-js-sdk:screen record shared popup has been closed', error);
					}
				);
				onStop();
			};
			// 保存开启屏幕录制的操作
			this.ccsClient &&
				this.ccsClient.screenRecord({}, this.ccsClient.clientId, status => {
					Logger.info('ivcs-js-sdk:save screenRecord operate status,', status);
				});
			this.recordingIds.push(recordId);
			onSuccess({
				publicationId: publication.id,
				recordId: recordId,
			});
		} catch (error) {
			Logger.info('ivcs-js-sdk:start record screen error', error);
			if (this.recordScreenStream) {
				stopStream(this.recordScreenStream);
			}
			onFailure(error);
		}
	}

	/**
	 * 停止屏幕录制
	 * @since 1.0.5版本更新API
	 * @param {string} publicationId 开启屏幕录制成功后返回的publicationId
	 * @param {string} recordId 开启屏幕录制成功后返回的recordId
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.stopRecordScreen(
			this.screenRecordPublicationId,
			this.screenRecordId,
			resp => {
				console.log('stop record screen success', resp);
			},
			error => {
				console.log('stop record screen error', error);
			}
		);
	 */
	async stopRecordScreen(publicationId, recordId, onSuccess, onFailure) {
		try {
			let accessToken = this.amsClient.accessToken;
			let backendUrl = this.backendUrl;
			this.recordingIds = this.recordingIds.filter(item => item !== recordId);
			let result = recordId ? await stopRecordStream(backendUrl, this.roomId, recordId, accessToken, false, this.sessionId) : '';
			this.stopScreenShare(
				publicationId,
				resp => {
					Logger.info('ivcs-js-sdk:stop screen share success', resp);
				},
				error => {
					Logger.info('ivcs-js-sdk:stop screen share error', error);
				}
			);
			// 保存停止屏幕录制的操作
			this.ccsClient &&
				this.ccsClient.stopScreenRecord({}, this.ccsClient.clientId, status => {
					Logger.info('ivcs-js-sdk:save stopScreenRecord operate status,', status);
				});
			onSuccess(result);
		} catch (error) {
			onFailure(error);
		}
	}

	/**
	 * 开启屏幕区域录屏，借助(chrome)屏幕共享功能,录制特定区域
	 * @param {object} dom DOM元素 eg:document.querySelector('#innerCropArea')
	 * @param {function} onSuccess 成功 {recordId:'',publicationId:''}
	 * @param {function} onFailure 失败 {code:'60028'} 60028:不支持非标签页的区域录屏
	 * @param {function} onStop 系统窗口被关闭（录制结束）
	 * @param {function} onRecordError 录制失败
	 */
	async startRecordRegionalScreen(dom, onSuccess, onFailure, onStop, onRecordError) {
		try {
			let streamOptions = {
				audioConstraints: deepCopy(this.mediaOptions.screenStreamAudioConstraints),
				videoConstraints: deepCopy(this.mediaOptions.screenStreamVideoConstraints),
			};
			streamOptions.videoConstraints.cropElement = dom;
			const recordScreenStreamLabel = 'recordScreen-Regional';
			const userInfo = {
				clientId: this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '',
				userName: this.agentName || '',
			};
			console.log('startRecordRegionalScreen streamOptions', streamOptions);
			let screenStream = await createScreenStream(this.irtcClient, streamOptions, recordScreenStreamLabel, userInfo);
			this.recordRegionalScreenStream = screenStream;
			this.irtcClient.createStreams.push(screenStream);
			let publishOptions = deepCopy(this.mediaOptions.screenPublishOptions);
			screenStream.mediaStream.getAudioTracks().length > 0 ? (publishOptions.audio ? 1 : (publishOptions.audio = deepCopy(this.mediaOptions.screenPublishOptions.audio))) : (publishOptions.audio = false);
			screenStream.mediaStream.getVideoTracks().length > 0 ? (publishOptions.video ? 1 : (publishOptions.video = deepCopy(this.mediaOptions.screenPublishOptions.video))) : (publishOptions.video = false);
			let publication = await publishStream(this.irtcClient, screenStream, publishOptions);
			let accessToken = this.amsClient.accessToken;
			let backendUrl = this.backendUrl;
			let audioId = screenStream.mediaStream.getAudioTracks() && screenStream.mediaStream.getAudioTracks().length > 0 ? publication.id : null;
			let videoId = screenStream.mediaStream.getVideoTracks() && screenStream.mediaStream.getVideoTracks().length > 0 ? publication.id : null;
			let needThrownException = true;
			const type = 'regional-screen';
			audioId = this.irtcClient.mixStreamId; // 录制mix流id的语音
			let recordId; // 录制id
			try {
				recordId = await recordStream(backendUrl, this.roomId, audioId, videoId, accessToken, needThrownException, this.sessionId, type);
			} catch (error) {
				onRecordError && onRecordError(error);
				onFailure(error);
				this.stopScreenShare(
					publication.id,
					resp => {
						Logger.info('ivcs-js-sdk:stop screen share success with record error', resp, new Date().toISOString());
					},
					error => {
						Logger.info('ivcs-js-sdk:stop screen share error with record error', error, new Date().toISOString());
					}
				);
				return;
			}
			screenStream.mediaStream.getVideoTracks()[0].onended = async () => {
				this.stopRecordRegionalScreen(
					publication.id,
					recordId,
					success => {
						Logger.warning('ivcs-js-sdk:regional screen record shared popup has been closed', success);
						recordId = null;
					},
					error => {
						Logger.warning('ivcs-js-sdk:regional screen record shared popup has been closed', error);
					}
				);
				onStop();
			};
			this.ccsClient &&
				this.ccsClient.startRecording({ type: type }, this.ccsClient.clientId, status => {
					Logger.info('ivcs-js-sdk:save startRecordRegionalScreen result', status);
				});
			this.recordingIds.push(recordId);
			onSuccess({
				publicationId: publication.id,
				recordId: recordId,
			});
		} catch (error) {
			Logger.info('ivcs-js-sdk:start record recordRegionalScreen error', error);
			if (this.recordRegionalScreenStream) {
				stopStream(this.recordRegionalScreenStream);
			}
			onFailure(error);
		}
	}

	/**
	 * 停止区域屏幕录制
	 * @param {string} publicationId 开启区域屏幕录制成功后返回的publicationId
	 * @param {string} recordId 开启区域屏幕录制成功后返回的recordId
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 */
	async stopRecordRegionalScreen(publicationId, recordId, onSuccess, onFailure) {
		try {
			let accessToken = this.amsClient.accessToken;
			let backendUrl = this.backendUrl;
			this.recordingIds = this.recordingIds.filter(item => item !== recordId);
			let result = recordId ? await stopRecordStream(backendUrl, this.roomId, recordId, accessToken, false, this.sessionId) : '';
			this.stopScreenShare(
				publicationId,
				resp => {
					Logger.info('ivcs-js-sdk:stop screen share success', resp);
				},
				error => {
					Logger.info('ivcs-js-sdk:stop screen share error', error);
				}
			);
			const type = 'regional-screen';
			this.ccsClient &&
				this.ccsClient.stopRecording({ type: type }, this.ccsClient.clientId, status => {
					Logger.info('ivcs-js-sdk:save stopRegionalScreenRecord operate status,', status);
				});
			onSuccess(result);
		} catch (error) {
			onFailure(error);
		}
	}

	/**
	 * 邀请其他坐席(三方)
	 * @param {object} inviteData 相关参数，指定业务场景/坐席id {service:'123',agentId:'123456'}
	 * @param {function} onSuccess 成功 {reason:"业务已完成"},默认传值为{}
	 * @param {function} onFailure 失败 {'400':'邀请无可用坐席！','401:'转接超时',402':'其他坐席状态无法接听','404':'未找到坐席','406':'转接的业务场景非工作日','407':'转接的业务场景非工作时间','408':'转接的业务场景不可用','412':'通话中存在多访客禁止邀请AI坐席','413':'坐席拒绝接听','414':'邀请振铃超时','416':访客角色为空（呼叫）;'417':场景角色为空（呼叫、转接）;'418':场景角色不匹配（呼叫、转接）;'419':会话中当前角色已存在;'420':指定了分组链呼叫时，对应业务场景下不包含分组链中的任何分组;'421':指定坐席并且指定了标签呼叫，派工时发现指定的坐席没有绑定该标签; '422': '转接的坐席未绑定当前业务场景对应的分组'， '423': '转接的坐席未签入当前业务场景对应的分组';}
	 * @param {function} onProgress 邀请中
	 * @example
	 * let option = {
				agentId: ,//指定的坐席id
				service: ,//业务场景id
				bizSceneName: '',//业务场景名称
				userData: {
					service: bizScenarioId,
				},
				filter: []
		};
		ivcs.agent.inviteOther(
					option,
					function(resp) {
						console.log('inviteOther success', resp);
					},
					function(error) {
						console.log('inviteOther error', error);
					}
		);
	 */
	async inviteOther(inviteData, onSuccess, onFailure, onProgress) {
		if (!inviteData.service) {
			inviteData.service = '';
		}
		if (!inviteData.agentId) {
			inviteData.agentId = '*';
		}
		inviteData.type = 'invite-multiple';
		// inviteData.status = 'invite-multiple-start';
		// await notifyTransference(this.ccsClient, inviteData);
		let listener = onInvitationProgress(this.ccsClient, async resp => {
			Logger.info('ivcs-js-sdk:invite other agent progress', resp, new Date());
			let code = resp.code;
			if (code === '405') {
				inviteData.status = 'invite-multiple-success';
				await notifyTransference(this.ccsClient, inviteData);
				this.ccsClient.offInvitationProgress(listener);
				onSuccess && onSuccess(resp);
			} else if (code === '401' || code === '404' || code === '406' || code === '407' || code === '408' || code === '409' || code === '410' || code === '411' || code === '412' || code === '413' || code === '414' || code === '416' || code === '417' || code === '418' || code === '419' || code === '420' || code === '421' || code === '422' || code === '423') {
				inviteData.status = 'invite-multiple-failed';
				inviteData.result = resp;
				await notifyTransference(this.ccsClient, inviteData);
				this.ccsClient.offInvitationProgress(listener);
				onFailure(resp);
			}
		});
		await inviteOtherAgent(this.ccsClient, inviteData)
			.then(async response => {
				if (response && response.hasAvailableAgent) {
					onSuccess && onSuccess('ok');
					//解决成功回调多次的问题
					onSuccess = null;
					inviteData.status = 'invite-multiple-start';
					await notifyTransference(this.ccsClient, inviteData);
					onProgress && onProgress();
				} else if (response && !response.hasAvailableAgent) {
					this.ccsClient.offInvitationProgress(listener);
					inviteData.type = 'invite-multiple';
					inviteData.status = 'invite-multiple-failed';
					inviteData.code = '400';
					await notifyTransference(this.ccsClient, inviteData);
					onFailure(new IvcsError('400', 'no available agent'));
				}
			})
			.catch(error => {
				this.ccsClient.offInvitationProgress(listener);
				onFailure(error);
			});
	}

	/**
	 * 转接给其他坐席
	 * @deprecated since version 1.1.8
	 * @param {object} inviteData 相关参数，指定业务场景/坐席id {service:'123',agentId:'123456'}
	 * @param {boolean} waitOtherAgentJoinSession 是否等待其他坐席加入会话后，再离开会话。默认是false(转接成功，其他坐席进入等待接听状态，当前坐席就挂断)
	 * @param {object} reason  挂断原因 {reason:"业务已完成"},默认传值为{}
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败 {'401:'转接超时',402':'其他坐席状态异常无法接听','404':'未找到坐席',
											'406':'转接的业务场景非工作日','407':'转接的业务场景非工作时间','408':'转接的业务场景不可用','601':'当前转接业务已完成，暂无法转接！'
										'602':'当前转接业务已完成，暂无法转接！','603':'当前转接业务正在进行中，暂无法转接！','604':'当前转接业务正在进行中，暂无法转接！','422': '转接的坐席未绑定当前业务场景对应的分组'， '423': '转接的坐席未签入当前业务场景对应的分组';}
	 * @example
	 * let option = {
				agentId: ,//指定的坐席id
				service: ,//业务场景id
				bizSceneName: '',//业务场景名称
				userData: {
					service: bizScenarioId,
				},
				filter: []
		};
	   ivcs.agent.transfer(
				option,
				false,
				transferReason,
				function(resp) {
					console.log('transfer success', resp);
				},
				function(error) {
					console.log('transfer error', error, error.code);
					}
				}
			);
	 */
	async transfer(inviteData, waitOtherAgentJoinSession = false, reason = {}, onSuccess, onFailure) {
		if (!inviteData.service) {
			inviteData.service = '';
		}
		if (!inviteData.agentId) {
			inviteData.agentId = '*';
		}
		inviteData.type = 'transfer-bizscenario';
		inviteData.status = 'start';
		await notifyTransference(this.ccsClient, inviteData);
		await inviteOtherAgent(this.ccsClient, inviteData).catch(async error => {
			inviteData.status = 'invite-error';
			await notifyTransference(this.ccsClient, inviteData);
			onFailure(error);
			return;
		});
		let listener = onInvitationProgress(this.ccsClient, async resp => {
			Logger.info('ivcs-js-sdk:transfer agent progress', resp, new Date());
			let code = resp.code;
			if (code === '401' || code === '404' || code === '406' || code === '407' || code === '408' || code === '409' || code === '410' || code === '411' || code === '412' || code === '413' || code === '414' || code === '416' || code === '417' || code === '418' || code === '419' || code === '420' || code === '421' || code === '422' || code === '423') {
				inviteData.status = 'end';
				await notifyTransference(this.ccsClient, inviteData);
				this.ccsClient.offInvitationProgress(listener);
				onFailure(resp);
				return;
			} else {
				try {
					if (code === '402') {
						// transfer error:agent ringing is disconnect
						if (resp.waitingCode == '1004' || (waitOtherAgentJoinSession && (resp.waitingCode == '1001' || resp.waitingCode == '1002' || resp.waitingCode == '1003'))) {
							inviteData.status = 'end';
							await notifyTransference(this.ccsClient, inviteData);
							this.ccsClient.offInvitationProgress(listener);
							this.cancelInvitations(
								resp => {
									Logger.info('ivcs-js-sdk:cancelInvitations success', resp, new Date());
								},
								error => {
									Logger.error('ivcs-js-sdk:cancelInvitations error', error, new Date());
								}
							);
							onFailure(resp);
							return;
						}
						//waiting status & not wait other agent join session
						if (!waitOtherAgentJoinSession) {
							this.ccsClient.offInvitationProgress(listener);
							inviteData.status = 'end';
							await notifyTransference(this.ccsClient, inviteData);
							this.hangup(
								reason,
								resp => {
									onSuccess(resp);
								},
								error => {
									onFailure(error);
								}
							);
							return;
						}
					} else if (code === '403') {
						if (!waitOtherAgentJoinSession) {
							this.ccsClient.offInvitationProgress(listener);
							inviteData.status = 'end';
							await notifyTransference(this.ccsClient, inviteData);
							this.hangup(
								reason,
								resp => {
									onSuccess(resp);
								},
								error => {
									onFailure(error);
								}
							);
							return;
						}
					} else if (code === '405') {
						this.ccsClient.offInvitationProgress(listener);
						inviteData.status = 'end';
						await notifyTransference(this.ccsClient, inviteData);
						this.hangup(
							reason,
							resp => {
								onSuccess(resp);
							},
							error => {
								onFailure(error);
							}
						);
						return;
					}
				} catch (error) {
					this.ccsClient.offInvitationProgress(listener);
					onFailure(error);
					return;
				}
			}
		});
	}

	/**
	 * 转接业务场景(可指定坐席工号，可不同业务场景转接)
	 * @param {object} inviteData 相关参数，指定业务场景/坐席id {service:'123',agentId:'123456',type:'transfer-bizscenario' || 'transfer-agent'}
	 * @param {function} onSuccess 'ok'  转接到可用坐席，坐席端自己执行挂断会话操作。
	 * @param {function} onFailure 失败 {'400':'无可用坐席','401:'转接超时',402':'其他坐席状态异常无法接听','404':'未找到坐席',
											'406':'转接的业务场景非工作日','407':'转接的业务场景非工作时间','408':'转接的业务场景不可用','416':访客角色为空（呼叫）;'417':场景角色为空（呼叫、转接）;'418':场景角色不匹配（呼叫、转接）;'419':会话中当前角色已存在;'420':指定了分组链呼叫时，对应业务场景下不包含分组链中的任何分组;'421':指定坐席并且指定了标签呼叫，派工时发现指定的坐席没有绑定该标签,  '422': '转接的坐席未绑定当前业务场景对应的分组'， '423': '转接的坐席未签入当前业务场景对应的分组''601':'当前转接业务已完成，暂无法转接！',
										'602':'当前转接业务已完成，暂无法转接！','603':'当前转接业务正在进行中，暂无法转接！','604':'当前转接业务正在进行中，暂无法转接！'}
	 */
	async transferBizscenario(inviteData, onSuccess, onFailure) {
		if (!inviteData.service) {
			inviteData.service = '';
		}
		if (!inviteData.agentId) {
			inviteData.agentId = '*';
		}
		inviteData.status = 'start';
		await notifyTransference(this.ccsClient, inviteData);
		let listener = onInvitationProgress(this.ccsClient, async resp => {
			Logger.info(`ivcs-js-sdk:transfer agent with parameter,${inviteData},progress,${resp},time:${new Date().toISOString()}`);
			let code = resp.code;
			if (code === '401' || code === '404' || code === '406' || code === '407' || code === '408' || code === '409' || code === '410' || code === '411' || code === '412' || code === '413' || code === '414' || code === '416' || code === '417' || code === '418' || code === '419' || code === '420' || code === '421' || code === '422' || code === '423') {
				inviteData.status = 'end';
				await notifyTransference(this.ccsClient, inviteData);
				this.ccsClient.offInvitationProgress(listener);
				onFailure(resp);
			}
		});
		await inviteOtherAgent(this.ccsClient, inviteData)
			.then(async response => {
				if (response && response.hasAvailableAgent) {
					// 有可用坐席则坐席挂断
					this.systemRecordId = null;
					onSuccess('ok');
					// 如果是转接不同业务场景，后端会把当前坐席从session中移除，所以坐席不需要自己quitSession了。
					// if (inviteData.service !== this.ringData.bizScenarioId && inviteData.type === 'transfer-bizscenario') {
					// 	releaseCcsClient(this.ccsClient);
					// }
				} else if (response && !response.hasAvailableAgent) {
					this.ccsClient.offInvitationProgress(listener);
					inviteData.type = 'transfer-bizscenario';
					inviteData.status = 'end';
					inviteData.code = '400';
					await notifyTransference(this.ccsClient, inviteData);
					onFailure(new IvcsError('400', 'no available agent'));
				}
			})
			.catch(async error => {
				inviteData.status = 'invite-error';
				await notifyTransference(this.ccsClient, inviteData);
				onFailure(error);
			});
	}

	/**
	 * 取消所有的invite（取消转接和三方）
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.cancelInvitations((s)=>{
			console.log('cancelInvitations success',s);
		},(e)=>{
			console.log('cancelInvitations error',e)
		})
	 */
	cancelInvitations(onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
		}
		this.ccsClient.cancelInvitations(undefined, async (status, resp, msgId) => {
			if (status === 'ok') {
				await notifyTransference(this.ccsClient, { status: 'invitation-canceled' });
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:ivcs-js-sdk:cancel invitations failed,${resp},msgId ${msgId},${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 对自己画面的暂停(停止数据传输)，支持音频/视频，暂停视频，则对方看不到自己实时画面;暂停音频，则对方听不到自己的实时声音
	 * @param {string} type  可选值 'video','audio'
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.mute(
			'audio',
			function() {
				console.log('mute audio succss');
			},
			function(error) {
				console.log('mute audio failed', error);
			}
		);
	 */
	async mute(type, onSuccess, onFailure) {
		try {
			await mute(this.localPublicationId, type, this.irtcClient);
			onSuccess();
		} catch (error) {
			onFailure(error);
		}
	}

	/**
	 * 自己画面的恢复(恢复数据传输)，支持音频/视频，恢复视频，则对方可以继续看到自己实时画面;恢复音频，则对方可以继续听到自己的实时声音
	 * @param {string} type 可选值 'video','audio'
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.client.unmute(
			'audio',
			function() {
				console.log('unmute audio succss');
			},
			function(error) {
				console.log('unmute audio failed', error);
			}
		);
	 */
	async unmute(type, onSuccess, onFailure) {
		try {
			await unmute(this.localPublicationId, type, this.irtcClient);
			onSuccess();
		} catch (error) {
			onFailure(error);
		}
	}

	/**
	 * 发送资源文件
	 * @param {string} type 文件类型 支持:'doc', 'pic', 'video', 'audio', 'page', 'paper'
	 * @param {FormData} formData  具体文件的数据
	 * @param {string} toRole 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.shareResource(
				'pic',
				 data,
				'others',
				resp => {
					console.log('shareResource resp', resp);
				},
				error => {
					console.log('send pic Message failed', error);
				}
			);
	 */
	async shareResource(type, formData, toRole = 'customers', onSuccess, onFailure) {
		let supportType = ['doc', 'pic', 'video', 'audio', 'page', 'paper'];
		if (supportType.indexOf(type) === -1) {
			let error = new IvcsError('2001', `type is invalid,only support 'doc','pic', 'video', 'audio', 'page', 'paper'`);
			onFailure(error);
			return;
		}
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
		}
		try {
			let url = this.backendUrl;
			let uploadResult = await uploadFile(url, formData, this.accessToken);
			this.ccsClient.shareResource(type, uploadResult.url, uploadResult.originalname, toRole, (status, resp, msgId) => {
				if (status === 'ok') {
					onSuccess(uploadResult);
				} else {
					Logger.info(`ivcs-js-sdk:send shareResource message failed,${resp},msgId${msgId},${new Date()}`);
					dealWithCcsSendMessageResponse(status, resp, onFailure);
				}
			});
		} catch (error) {
			onFailure(error);
		}
	}

	/**
	 * 根据文件的url发送文件。
	 * @param {string} type 文件类型 支持:'doc', 'pic', 'video', 'audio', 'page', 'paper'
	 * @param {string} url 文件相应的url地址
	 * @param {string} toRole 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {string} fileName 文件名
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.shareResourceWithUrl(
				'pic',
				 url,
				'others',
				'fileName'
				resp => {
					console.log('shareResourceWithFileInfo resp', resp);
				},
				error => {
					console.log('send pic Message failed', error);
				}
			);
	 */
	async shareResourceWithUrl(type, url, toRole = 'customers', fileName = '', onSuccess, onFailure) {
		let supportType = ['doc', 'pic', 'video', 'audio', 'page', 'paper'];
		if (supportType.indexOf(type) === -1) {
			let error = new IvcsError('2001', `type is invalid,only support 'doc','pic', 'video', 'audio', 'page', 'paper'`);
			onFailure(error);
			return;
		}
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
		}
		try {
			this.ccsClient.shareResource(type, url, fileName, toRole, (status, resp, msgId) => {
				if (status === 'ok') {
					onSuccess(resp);
				} else {
					Logger.info(`ivcs-js-sdk:send shareResource message failed,${resp},msgId${msgId},${new Date()}`);
					dealWithCcsSendMessageResponse(status, resp, onFailure);
				}
			});
		} catch (error) {
			onFailure(error);
		}
	}

	/**
	 * 根据文件详情发送文件资源<主要用于IM里面发送文件图片等>
	 * @param {string} type 文件类型 支持:'doc', 'pic', 'video', 'audio', 'page', 'paper'
	 * @param {object} fileData 文件对象 { name: 文件名称(String), size: 文件大小(Number), url: 文件地址(String) }
	 * @param {string} toRole 接受文件的用户，'customers'= 访客;'agents'= 坐席;'all'= 所有人，'others'= 除了自己，所有人,  默认是'customers';
	 * @param {function} onSuccess 成功回调
	 * @param {function} onFailure 失败回调
	 * @example
	 * ivcs.agent.shareResourceWithFileInfo(
				'pic',
				data,
				'others',
				resp => {
					console.log('shareResourceWithFileInfo resp', resp);
				},
				error => {
					console.log('send pic Message failed', error);
				}
			);
	 */
	async shareResourceWithFileInfo(type, fileData, toRole = 'customers', onSuccess, onFailure) {
		let supportType = ['doc', 'pic', 'video', 'audio', 'page', 'paper'];
		if (supportType.indexOf(type) === -1) {
			let error = new IvcsError('2001', `type is invalid,only support 'doc','pic', 'video', 'audio', 'page', 'paper'`);
			onFailure(error);
			return;
		}
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
		}
		try {
			this.ccsClient.shareResourceWithFileInfo(type, fileData, toRole, (status, resp, msgId) => {
				if (status === 'ok') {
					onSuccess(resp);
				} else {
					Logger.info(`ivcs-js-sdk:send shareResourceWithFileInfo message failed,${resp},msgId${msgId},${new Date()}`);
					dealWithCcsSendMessageResponse(status, resp, onFailure);
				}
			});
		} catch (error) {
			onFailure(error);
		}
	}

	/**
	 * 发送tts辅助播报文字消息，需要在订阅到tts语音流，监听onTTSStreamSpeaking后使用
	 * @param {string} message 消息内容
	 * @param {string} toRole 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.sendTTSBroadMessage(
				message,
				undefined,
				resp => {
					console.log('send success', resp);
				},
				error => {
					console.log('send error', error);
				}
			);
	 */
	sendTTSBroadMessage(message, toRole = 'all', onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
		}
		this.ccsClient.sendTTSBroadMessage(message, toRole, (status, resp, msgId) => {
			if (status === 'ok') {
				onSuccess('ok');
			} else {
				Logger.info(`ivcs-js-sdk:send tts text message:${message} to ${toRole} failed,reason:${resp},msgId:${msgId},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 留痕开始tts辅助播报
	 * @param {string} message 播报的内容
	 * @param {string} toRole 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @ignore
	 */
	startTtsSpeaking(message, toRole = 'all', onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
		}
		this.ccsClient.startTtsSpeaking(message, toRole, (status, resp, msgId) => {
			if (status === 'ok') {
				onSuccess('ok');
			} else {
				Logger.info(`ivcs-js-sdk:send startTtsSpeaking message:${message} to ${toRole} failed,reason:${resp},msgId:${msgId},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 留痕取消tts辅助播报
	 * @param {string} message 播报的内容
	 * @param {string} toRole 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @ignore
	 */
	cancelTtsSpeaking(message, toRole = 'all', onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
		}
		this.ccsClient.cancelTtsSpeaking(message, toRole, (status, resp, msgId) => {
			if (status === 'ok') {
				onSuccess('ok');
			} else {
				Logger.info(`ivcs-js-sdk:send cancelTtsSpeaking message:${message} to ${toRole} failed,reason:${resp},msgId:${msgId},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 发送文字消息
	 * @param {string} message 消息内容
	 * @param {string} toRole 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.sendTextMessage(
				message,
				undefined,
				resp => {
					console.log('send success', resp);
				},
				error => {
					console.log('send error', error);
				}
			);
	 */
	sendTextMessage(message, toRole = 'customers', onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
		}
		this.ccsClient.sendTextMessage(message, toRole, (status, resp, msgId) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.info(`ivcs-js-sdk:send text message:${message} failed,reason:${resp},msgId${msgId},${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 收到文字消息
	 * @param {function} ack 具体的消息数据
	 * @example
	 * ivcs.agent.onTextMessage(resp => {
			console.log('ontextmessage',resp);
		});
	 */
	onTextMessage(ack) {
		this.ccsClient.onTextMessage(data => {
			ack(data);
		});
	}

	/**
	 * 监听会话中的变量发生变化
	 * @param {function} ack {id(变量id),value(变量值),roleId(角色id),roleName(角色名字)}
	 * @example
	 * ivcs.agent.onVariableChange(resp => {
			console.log('onVariableChange',resp);
		});
	 */
	onVariableChange(ack) {
		this.ccsClient.onVariableChange(data => {
			ack(data);
		});
	}

	/**
	 * 监听智能识别的文本，如asr，tts，预录制视频的文本
	 * @param {function} ack 返回内容 {createdAt:string(),data:string(),from:string(clientId),fromUser:Object(),to:string(),type:string('text-asr','text-tts','text-pre-recorded')}
	 * @example
	 * ivcs.agent.onAiRecognitionMessage(resp => {
			if (resp && resp.data) {
				this.textMessage = resp.data;
			}
			this.textType = resp.type; // 'text-asr','text-tts','text-pre-recorded'	
		});
	 */
	onAiRecognitionMessage(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onAiRecognitionMessage error', isValid);
			return;
		}
		this.ccsClient.onAsrTextMessage(ack);
		// 后端text-ttx数据结构变更
		this.ccsClient.onTtsTextMessage(data => {
			let resultData = deepCopy(data);
			resultData.data = data?.data?.content ? data?.data?.content : data?.data;
			ack(resultData);
		});
		this.ccsClient.onPreRecordVideoTextMessage(ack);
	}

	/**
	 * 收到资源文件信息（图片，文档等）
	 * @param {function} ack 返回具体的资源文件数据
	 * @example
	 * ivcs.agent.onResourceMessage(resp => {
			console.log('receive resource message,', resp);
		});
	 */
	onResourceMessage(ack) {
		this.ccsClient.onSharedResource(data => {
			ack(data);
		});
	}

	/**
	 * 监听session中其他用户ccs重连成功,用于检测对方网络是否重连成功
	 * @param {function} ack 
		data:{
		  id: client.id,
		  status: 'resmue-connect',
		  role: client.role,
		  user: client.user,
		  at: moment().toISOString()
		}
	 * @example
		ivcs.agent.onOtherResumeConnect(data => {
		});
	 */
	onOtherResumeConnect(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onOtherResumeConnect error', isValid);
			return;
		}
		this.ccsClient.onClientResumeConnectStatus(resp => {
			ack(resp);
		});
	}

	/**
	 * 监听session中其他用户ccs重连中,用于检测对方网络是否断开在重连中
	 * @param {function} ack
		  data:{
		  id: client.id,
		  status: 'reconnecting',
		  role: client.role,
		  user: client.user,
		  at: moment().toISOString()
		}
	 * @example
		ivcs.agent.onOtherReconnecting(data => {
		});
	 */
	onOtherReconnecting(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onOtherReconnecting error', isValid);
			return;
		}
		this.ccsClient.onClientReconnectingStatus(resp => {
			ack(resp);
		});
	}

	/**
	 * 监听session中其他用户页面变化情况,用于检测对方是否页面隐藏(手机切到后台等情况)
	 * @param {function} ack {status:'hidden'|'visiable'}
	 * @returns null
	 */
	onOtherPageStatus(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onOtherPageStatus error', isValid);
			return;
		}
		this.ccsClient.onPageStatus(resp => {
			ack(resp);
		});
	}

	/**
	 * 监听session中其他用户弱网情况,用于检测对方网络变弱/恢复情况
	 * @param {function} ack
	 * @returns null
	 */
	onOtherNetworkStatus(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onOtherNetworkStatus error', isValid);
			return;
		}
		this.ccsClient.onNetworkStatus(resp => {
			ack(resp);
		});
	}

	/**
	 * 发送自定义消息
	 * @param {string|object} data 自定义的消息内容
	 * @param {string} toRole 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.sendCustomMessage(
				data,
				role,
				success => {
					console.log('发送消息成功！' + JSON.stringify(data), success);
				},
				error => {
					console.log('发送消息失败！' + JSON.stringify(data), error);
				}
			);
	 */
	sendCustomMessage(data, toRole = 'customers', onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
		}
		this.ccsClient.sendCustomMessage(data, toRole, (status, resp, msgId) => {
			if (status === 'ok') {
				onSuccess('ok');
			} else {
				Logger.info(`ivcs-js-sdk:send custom message: ${data} to ${toRole} failed,reason:${resp},msgId ${msgId},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 收到自定义消息
	 * @param {function} ack 返回具体的消息内容
	 * @example
	 * ivcs.agent.onCustomMessage(resp => {
				if (resp.data.businessType == 'mark_trace') {
					let code = resp.data.message.track;
					console.log('startScreen mark code', code);
				}
				...
			});
	 */
	onCustomMessage(ack) {
		onCustomMessage(this.ccsClient, data => {
			ack(data);
		});
	}

	/**
	 * 邀请访客屏幕共享,目前只支持安卓原生app
	 * @param {string} toRole 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.viewScreen(
				'customers',
				resp => {
					console.log('view screen success', resp);
				},
				error => {
					console.log('view screen error', error);
				}
			);
	 */
	viewScreen(toRole = 'customers', onSuccess, onFailure) {
		requestScreenShareWithOthers(
			this.ccsClient,
			toRole,
			resp => {
				onSuccess(resp);
			},
			error => {
				onFailure(error);
			}
		);
	}

	/**
	 * 取消远程访问访客屏幕,目前只支持安卓原生app
	 * @param {string} toRole 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.releaseScreen(
				'customers',
				resp => {
					console.log('release screen success', resp);
				},
				error => {
					console.log('release screen error', error);
				}
			);
	 */
	releaseScreen(toRole = 'customers', onSuccess, onFailure) {
		releaseScreenShare(
			this.ccsClient,
			toRole,
			resp => {
				onSuccess(resp);
			},
			error => {
				onFailure(error);
			}
		);
	}

	/**
	 * 标注远程屏幕的画面坐标,目前只支持web端基于fabric框架标注
	 * @param {string} toRole 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {string|number} x 点击坐标x
	 * @param {string|number} y 点击坐标y
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.markScreen(
				undefined,//发送给的用户id。默认是‘customers’
				point.x * 3,//坐标x
				point.y * 3,//坐席y
				resp => {
					console.log('mark screen success',resp);
				},
				error => {
					console.log('mark screen error',error);
				}
			);
	 */
	markScreen(toRole = 'customers', x, y, onSuccess, onFailure) {
		let data = {
			x: x,
			y: y,
		};
		markScreen(
			this.ccsClient,
			data,
			toRole,
			resp => {
				onSuccess(resp);
			},
			error => {
				onFailure(error);
			}
		);
	}

	/**
	 * 收到访客告知其屏幕大小事件消息，可在UI上标注访客屏幕画面，目前只支持安卓原生app
	 * @param {function} ack 返回访客端的屏幕尺寸大小
	 * @example
	 * ivcs.agent.onDeviceScreenSize(resp => {
				if (resp && resp.data) {
					let size = JSON.parse(resp.data);
					this.customerScreenSize.width = size.w / 3;
					this.customerScreenSize.height = size.h / 3;
					console.log('device screen size', resp, size);
				}
		});
	 */
	onDeviceScreenSize(ack) {
		onDeviceScreenSize(this.ccsClient, resp => {
			ack(resp);
		});
	}

	/**
	 * 发送转人工原因消息
	 * @param {object} message 转人工原因的具体消息内容 如{reason:"转人工原因"}
	 * @param {string} toRole  发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 成功回调
	 * @param {function} onFailure 失败回调
	 * @example
	 * ivcs.agent.sendTransferReasonMessage(
				{reason:""},
				clientId,
				res => {
					console.log('sendTransferReasonMessage success',res);
				},
				error => {
					console.log('sendTransferReasonMessage error',error);
				}
			);
	 */
	sendTransferReasonMessage(message, toRole = 'customers', onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.sendTransferReason(message, toRole, (status, resp, msgId) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error('ivcs-js-sdk:sendTransferReason error', status, resp, msgId);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 收到转人工的消息
	 * @param {function} ack  返回转人工的原因消息
	 * @example
	 * ivcs.agent.onTransferReasonMessage(data => {
			console.log('onTransferReason data', data);
		});
	 */
	onTransferReasonMessage(ack) {
		this.ccsClient.onTransferReason(data => {
			ack(data);
		});
	}

	/**
	 * 收到当前会话发生业务变化，如坐席推送了扫码，访客拒绝扫码等指令或者坐席操作
	 * @param {function} ack  返回具体数据  ack.data:{
				createdAt:'',
				data:{createdAt:'',data:{},fromClientId:'',fromUserId:'',fromUserInfo:{},fromUserName:'',fromUserRole:'',msgType:'',toUserInfo:{}},
				from:'system',
				fromUser:'',
				id: "830969657034878800"
				to: "all"
				type: "sync-message"
			} 
	 * @example
	 * ivcs.agent.onSyncMessage(data => {
			console.log('onSyncMessage data', data);
		});
	 */
	onSyncMessage(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onSyncMessage error', isValid);
			return;
		}
		this.ccsClient.onSyncMessage(data => {
			ack(data);
		});
	}

	/**
	 * 监听实时质检结果返回 如人脸比对 人证对比 敏感词
	 * @param {function} ack 返回具体的数据
	 */
	onRealTimeQuality(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onRealTimeQuality error', isValid);
			return;
		}
		this.ccsClient.onRealTimeQuality(data => {
			ack(data);
		});
	}

	/**
	 * 会话中系统异常,会话被系统终止,网络异常等情况
	 * @param {function} ack 返回具体的会话中异常情况
	 * @example
	 * ivcs.agent.onSessionDrop(resp => {
			console.log('on session drop', resp);
		});
	 */
	onSessionDrop(ack) {
		let dropMessage = {
			drop: {
				code: 401,
				message: 'socket io link for the session is disconnected by system',
				data: '',
			},
			error: {
				code: 402,
				message: 'socket io link for the session is disconnected',
			},
			onDisconnect: {
				code: 404,
				message: 'ccs socketio is disconnected',
			},
			onReconnectFailed: {
				code: 405,
				message: 'ccs socketio is reconnect failed',
			},
		};
		// eslint-disable-next-line no-unused-vars
		this.ccsClient.onDrop(resp => {
			releaseCcsClient(this.ccsClient);
			Logger.error('ivcs-js-sdk:on ccs Drop', resp);
			//fix 被系统强制结束，移除事件，防止下一笔会话多次触发irtc事件。
			zmsRemoveEventListener(this.irtcClient);
			dropMessage.drop.data = resp;
			ack(dropMessage.drop);
		});
		this.ccsClient.onError(async data => {
			if (data && data.code === '1101') {
				//token expired
				let newTokenTicket = await refreshNewToken(this.backendUrl, this.userId, this.ticket);
				this.ticket = newTokenTicket.ticket;
				this.accessToken = newTokenTicket.accessToken;
				// eslint-disable-next-line no-unused-vars
				let newConnect = await connect(this.ccsClient, this.ccsUrl, this.accessToken);
				// eslint-disable-next-line no-unused-vars
				let resumeConnect = resumeCcsConnection(this.ccsClient, this.ccsClient.ticket);
			} else {
				releaseCcsClient(this.ccsClient);
				ack(dropMessage.error);
			}
		});
		// eslint-disable-next-line no-unused-vars
		this.ccsClient.onDisconnect(error => {
			releaseCcsClient(this.ccsClient);
			ack(dropMessage.onDisconnect);
		});
		// eslint-disable-next-line no-unused-vars
		this.ccsClient.onReconnectFailed(error => {
			releaseCcsClient(this.ccsClient);
			ack(dropMessage.onReconnectFailed);
		});
	}

	/**
	 * 发送确认消息，告知对方相应的确认信息
	 * @param {string|object} message 确认消息的具体内容
	 * @param {string} toRole 发送消息的用户，'customers'= 访客;'agents'= 坐席;'all'= 除了自己的所有人，默认是'agents';
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 *  let data = 'ok';
		let id = clientId ? clientId : 'customers'; //判断clientId是否存在
		ivcs.agent.sendMessageConfirmation(
			data,
			id,
			resp => {
				console.log('send confirmation message success', resp);
			},
			error => {
				console.log('send confirmation message error', error);
			}
		);
	 */
	sendMessageConfirmation(message, toRole = 'customers', onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		// eslint-disable-next-line no-unused-vars
		this.ccsClient.sendMessageConfirmation(message, toRole, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 收到信息确认消息
	 * @param {function} ack 返回信息确认的具体数据
	 * @example
	 * ivcs.agent.onMessageConfirmation(data => {
		  console.log(data);
		);
	 */
	onMessageConfirmation(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.warning(`ivcs-js-sdk:onMessageConfirmation error,${JSON.stringify(isValid)}`);
			return;
		}
		this.ccsClient.onMessageConfirmation(data => {
			ack(data);
		});
	}

	/**
	 * 发送取消事件指令
	 * @deprecated since version 1.0.4
	 * @param {string|object} instructionData  instructionName 指令名 | {instructionName:'',data:''}
	 * @param {string} toRole 发送消息的用户，'customers'= 访客;'agents'= 坐席;'all'= 除了自己的所有人，默认是'agents';
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 */
	sendCancelInstructionMessage(instructionData, toRole, onSuccess, onFailure) {
		cancelInstruction(
			this.ccsClient,
			instructionData,
			toRole,
			s => {
				onSuccess(s);
			},
			e => {
				onFailure(e);
			}
		);
	}

	/**
	 * 收到取消/拒绝事件的指令，如:访客取消扫码。
	 * @deprecated since version 1.0.4
	 * @param {function} ack 收到取消事件指令,data.type即事件名
	 */
	onCancelInstruction(ack) {
		onCancelInstruction(this.ccsClient, data => {
			ack(data);
		});
	}

	/***************** 2021-12-25 1.0.5版本新增API，重构业务指令 */

	/**
	 * 保存录制屏幕业务操作 <目前只支持chrome浏览器，基于屏幕共享的录制屏幕>
	 * @since 1.0.5版本更新API
	 * @deprecated since version 1.1.2
	 * @ignore
	 * @param {object} cmdData 指令内容
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 */
	saveRecordScreen(cmdData, toWhom) {
		return new Promise((resolve, reject) => {
			let isValid = validateCcsClient(this.ccsClient);
			if (isValid != 'ok') {
				reject(isValid);
				return;
			}
			let askForPermission = false;
			this.ccsClient.sendPeerCmd('screen-record', askForPermission, cmdData, toWhom, (status, resp) => {
				if (status === 'ok') {
					resolve(status);
				} else {
					Logger.error(`ivcs-js-sdk:ivcs-js-sdk:recordScreen,error:${resp},at:${new Date()}`);
					reject(dealWithCcsSendMessageResponse(status, resp, null));
				}
			});
		});
	}

	/**
	 * 保存停止录制屏幕业务操作
	 * @since 1.0.5版本更新API
	 * @deprecated since version 1.1.2
	 * @ignore
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 */
	saveCancelRecordScreen(cmdId, toWhom) {
		return new Promise((resolve, reject) => {
			let isValid = validateCcsClient(this.ccsClient);
			if (isValid != 'ok') {
				reject(isValid);
				return;
			}
			this.ccsClient.sendPeerCancel('screen-record', cmdId, toWhom, (status, resp) => {
				if (status === 'ok') {
					resolve(status);
				} else {
					Logger.error(`ivcs-js-sdk:ivcs-js-sdk:cancelRecordScreen,error:${resp},at:${new Date()}`);
					reject(dealWithCcsSendMessageResponse(status, resp, null));
				}
			});
		});
	}

	/**
	 * 保存屏幕共享业务操作 <目前只支持chrome浏览器>  不走业务指令
	 * @since 1.0.5版本更新API
	 * @deprecated since version 1.1.2
	 * @ignore
	 * @param {object} cmdData 指令内容
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 */
	saveScreenShare(cmdData, toWhom) {
		return new Promise((resolve, reject) => {
			let isValid = validateCcsClient(this.ccsClient);
			if (isValid != 'ok') {
				reject(isValid);
				return;
			}
			let askForPermission = false;
			this.ccsClient.sendPeerCmd('screen-share', askForPermission, cmdData, toWhom, (status, resp) => {
				if (status === 'ok') {
					resolve(status);
				} else {
					Logger.error(`ivcs-js-sdk:saveScreenShare,error:${resp},at:${new Date()}`);
					reject(dealWithCcsSendMessageResponse(status, resp, null));
				}
			});
		});
	}

	/**
	 * 保存停止屏幕共享业务操作
	 * @since 1.0.5版本更新API
	 * @deprecated since version 1.1.2
	 * @ignore
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 */
	saveCancelScreenShare(cmdId, toWhom) {
		return new Promise((resolve, reject) => {
			let isValid = validateCcsClient(this.ccsClient);
			if (isValid != 'ok') {
				reject(isValid);
				return;
			}
			this.ccsClient.sendPeerCancel('screen-share', cmdId, toWhom, (status, resp) => {
				if (status === 'ok') {
					resolve(status);
				} else {
					Logger.error(`ivcs-js-sdk:ivcs-js-sdk:saveCancelScreenShare,error:${resp},at:${new Date()}`);
					reject(dealWithCcsSendMessageResponse(status, resp, null));
				}
			});
		});
	}

	/**
	 * 保存人脸识别后的结果
	 * @since 1.0.5版本更新API
	 * @param {object} cmdData 指令内容 默认为undefined
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.faceRecognition(
			result,
			this.customer.clientId,
			res => {
				console.log(res);
			},
			error => {
				console.log(error);
			}
		);
	 */
	faceRecognition(cmdData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		let askForPermission = false;
		this.ccsClient.sendPeerCmd('face-recognition', askForPermission, cmdData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:ivcs-js-sdk:faceRecognition,error:${resp},at:${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 保存人证对比后的结果
	 * @param {object} cmdData 指令内容 默认为undefined
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.faceIdcardValidate(
			{status:'ok',url:''},
			this.customer.clientId,
			res => {
				console.log(res);
			},
			error => {
				console.log(error);
			}
		);
	 */
	faceIdcardValidate(cmdData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		let askForPermission = false;
		this.ccsClient.sendPeerCmd('face-idcard-validate', askForPermission, cmdData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:faceIdcardValidate,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 开启屏幕共享标注<目前只支持h5，坐席对自己共享屏幕标注一些轨迹等>
	 * @since 1.0.5版本更新API
	 * @param {object} cmdData 指令内容
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.screenMark(
				undefined,
				this.customer.clientId,
				res => {
					console.log('send screenMark success', res);
				},
				error => {
					console.log('send screenMark error', error);
				}
			);
	 */
	screenMark(cmdData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		let askForPermission = false;
		this.ccsClient.sendPeerCmd('screen-mark', askForPermission, cmdData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:screenMark,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 取消屏幕共享标注<目前只支持h5，坐席对自己共享屏幕标注一些轨迹等>
	 * @since 1.0.5版本更新API
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelScreenMark(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('stop screenMark success', res);
			},
			error => {
				console.log('stop screenMark error', error);
			}
		);
	 */
	cancelScreenMark(cmdId, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.sendPeerCancel('screen-mark', cmdId, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:cancelScreenMark,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 接收访客响应共享标注过程/结果<目前只支持h5>
	 * @since 1.0.5版本更新API
	 * @param {function} ack 返回访客的响应结果
	 * @example
	 * ivcs.agent.onScreenMark(data => {
		 console.log(data);
		});
	 */
	onScreenMark(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onScreenMark error', isValid);
			return;
		}
		this.ccsClient.onPeerResponse('screen-mark', resp => {
			ack(resp);
		});
	}

	/**
	 * 开启远程协助<目前只支持协助安卓App>
	 * @since 1.0.5版本更新API
	 * @param {object} cmdData 指令内容
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.remoteControl(
			undefined,//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	remoteControl(cmdData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		let askForPermission = false;
		this.ccsClient.sendPeerCmd('remote-control', askForPermission, cmdData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:remoteControl,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 取消远程协助<目前只支持协助安卓App>
	 * @since 1.0.5版本更新API
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelRemoteControl(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('cancelRemoteControl success', res);
			},
			error => {
				console.log('cancelRemoteControl error', error);
			}
		);
	 */
	cancelRemoteControl(cmdId, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.sendPeerCancel('screen-sync', cmdId, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:cancelRemoteControl,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 推送引导框
	 * @since 1.0.5版本更新API
	 * @param {object} cmdData 指令内容 {type: enum('face','idcard-front','idcard-back','bank-card','drive-licence'...)}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.sendGuideBox(
			undefined,//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	sendGuideBox(cmdData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		let askForPermission = false;
		this.ccsClient.sendPeerCmd('show-guide-box', askForPermission, cmdData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:sendGuideBox,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 取消引导框
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelGuideBox(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('cancelGuideBox success', res);
			},
			error => {
				console.log('cancelGuideBox error', error);
			}
		);
	 */
	cancelGuideBox(cmdId, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.sendPeerCancel('show-guide-box', cmdId, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:cancelGuideBox,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 接收访客引导框操作的过程/结果
	 * @since 1.0.5版本更新API
	 * @param {function} ack 返回访客的响应结果
	 * @example
	 * ivcs.agent.onGuideBox(data => {
		 console.log(data);
		});
	 */
	onGuideBox(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onGuideBox error', isValid);
			return;
		}
		this.ccsClient.onPeerResponse('show-guide-box', resp => {
			ack(resp);
		});
	}

	/**
	 * 收集访客信息<本API是演示通话中采集访客信息的功能>
	 * @since 1.0.5版本更新API
	 * @param {object} cmdData 指令内容 {item:enum('basic','idcardFront','idcardBack','bank','self')}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.collectUserInfo(
			{item:'self'},//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	collectUserInfo(cmdData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		let askForPermission = false;
		this.ccsClient.sendPeerCmd('collect-info', askForPermission, cmdData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:collectUserInfo,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 推送资源（合同/网页/音频/视频/图片/其他）<AI推送查阅等待的条形码>
	 * @since 1.0.5版本更新API
	 * @param {object} cmdData 指令内容 {url:string(ResourceUrl),name: string(ResourceName),type: enum('paper','page','audio','video','pic','other')},{qrcode:string(qr-code)}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 *  let info = {};
		info.url = url;
		info.name = url;
		info.type = 'video';
		ivcs.agent.sendViewResource(
			info,
			clientId,
			res => {
				console.log('shareResourceWithUrl', res);
			},
			error => {
				console.log(error);
			}
		);
	 */
	sendViewResource(cmdData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		let askForPermission = false;
		this.ccsClient.sendPeerCmd('view', askForPermission, cmdData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:sendViewResource,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 取消推送资源,停止查看（合同/网页/音频/视频/图片/其他）<AI推送查阅等待的条形码>
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelViewResource(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('cancelViewResource success', res);
			},
			error => {
				console.log('cancelViewResource error', error);
			}
		);
	 */
	cancelViewResource(cmdId, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.sendPeerCancel('view', cmdId, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:cancelViewResource,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 接收访客查看资源过程/结果
	 * @since 1.0.5版本更新API
	 * @param {function} ack 返回访客的响应结果
	 * @example
	 * ivcs.agent.onViewResource(data => {
		console.log(data);
		});
	 */
	onViewResource(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onViewResource error', isValid);
			return;
		}
		this.ccsClient.onPeerResponse('view', resp => {
			ack(resp);
		});
	}

	/**
	 * 截图
	 * @since 1.0.5版本更新API
	 * @param {object} cmdData 指令内容 默认=undefined
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.screenshot(
			undefined,//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	screenshot(cmdData = undefined, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		let askForPermission = false;
		this.ccsClient.sendPeerCmd('get-screenshot', askForPermission, cmdData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:screenshot,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 取消截图
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelScreenshot(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('cancelScreenshot success', res);
			},
			error => {
				console.log('cancelScreenshot error', error);
			}
		);
	 */
	cancelScreenshot(cmdId, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.sendPeerCancel('get-screenshot', cmdId, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:cancelScreenshot,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 接收访客的截图过程/结果
	 * @since 1.0.5版本更新API
	 * @param {function} ack 返回访客的响应结果
	 * @example
	 * ivcs.agent.onScreenShot(data => {
		console.log(data);
		});
	 */
	onScreenShot(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onScreenShot error', isValid);
			return;
		}
		this.ccsClient.onPeerResponse('get-screenshot', resp => {
			ack(resp);
		});
	}

	/**
	 * 截屏
	 * @param {object} cmdData 指令内容 默认=undefined
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.screenCapture(
			undefined,//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	screenCapture(cmdData = undefined, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		let askForPermission = false;
		this.ccsClient.sendPeerCmd('get-screencapture', askForPermission, cmdData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:screenCapture,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 取消截屏
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelScreencapture(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('cancelScreenshot success', res);
			},
			error => {
				console.log('cancelScreenshot error', error);
			}
		);
	*/
	cancelScreencapture(cmdId, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.sendPeerCancel('get-screencapture', cmdId, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:cancelScreencapture,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 接收访客的截屏结果
	 * @param {function} ack 返回访客的响应结果
	 * @example
	 * ivcs.agent.onScreenCapture(data => {
		console.log(data);
		});
	*/
	onScreenCapture(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onScreenCapture error', isValid);
			return;
		}
		this.ccsClient.onPeerResponse('get-screencapture', resp => {
			ack(resp);
		});
	}

	/**
	 * 推送在线表单
	 * @since 1.0.5版本更新API
	 * @param {object} cmdData 指令内容 {formId:string(fomId),sessionId:string(sessionId)}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.sendOnlineForm(
			{formId:string(fomId),sessionId:string(sessionId)},//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	sendOnlineForm(cmdData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		let askForPermission = false;
		this.ccsClient.sendPeerCmd('fill-form', askForPermission, cmdData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:sendOnlineForm,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 取消访客在线表单填写
	 * @since 1.0.5版本更新API
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelOnlineForm(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('cancelOnlineForm success', res);
			},
			error => {
				console.log('cancelOnlineForm error', error);
			}
		);
	 */
	cancelOnlineForm(cmdId, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.sendPeerCancel('fill-form', cmdId, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:cancelOnlineForm,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 接收访客填写的表单过程/结果
	 * @since 1.0.5版本更新API
	 * @param {function} ack 返回访客的响应结果
	 * @example
	 * ivcs.agent.onFillForm(data => {
		 console.log(data);
		});
	 */
	onFillForm(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onFillForm error', isValid);
			return;
		}
		this.ccsClient.onPeerResponse('fill-form', resp => {
			ack(resp);
		});
	}

	/**
	 * 邀请手写签名
	 * @since 1.0.5版本更新API
	 * @param {object} cmdData 指令内容,默认是undefined
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.inviteHandWriting(
			undefined,//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	inviteHandWriting(cmdData = undefined, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		let askForPermission = false;
		this.ccsClient.sendPeerCmd('get-handwriting', askForPermission, cmdData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:inviteHandWriting,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 取消访客手写签名
	 * @since 1.0.5版本更新API
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelHandWriting(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('cancelHandWriting success', res);
			},
			error => {
				console.log('cancelHandWriting error', error);
			}
		);
	 */
	cancelHandWriting(cmdId, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.sendPeerCancel('get-handwriting', cmdId, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:cancelHandWriting,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 接收访客签名的过程/结果
	 * @since 1.0.5版本更新API
	 * @param {function} ack 返回访客的响应结果
	 * @example
	 * ivcs.agent.onHandWriting(data => {
		 console.log(data);
		});
	 */
	onHandWriting(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onHandWriting error', isValid);
			return;
		}
		this.ccsClient.onPeerResponse('get-handwriting', resp => {
			ack(resp);
		});
	}

	/**
	 * 请求获取访客地理位置
	 * @since 1.0.5版本更新API
	 * @param {object} cmdData 指令内容 默认是undefined
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.requestCustomerLocation(
			undefined,//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	requestCustomerLocation(cmdData = undefined, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		let askForPermission = false;
		this.ccsClient.sendPeerCmd('get-geo-location', askForPermission, cmdData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:requestCustomerLocation,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 取消获取访客地理位置
	 * @since 1.0.5版本更新API
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelRequestCustomerLocation(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('cancelRequestCustomerLocation success', res);
			},
			error => {
				console.log('cancelRequestCustomerLocation error', error);
			}
		);
	 */
	cancelRequestCustomerLocation(cmdId, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.sendPeerCancel('get-geo-location', cmdId, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:cancelRequestCustomerLocation,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 接收访客获取地理位置的过程/结果
	 * @since 1.0.5版本更新API
	 * @param {function} ack 返回访客的响应结果
	 * @example
	 * ivcs.agent.onGeoLocation(data => {
		  console.log(data);
		});
	 */
	onGeoLocation(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onGeoLocation error', isValid);
			return;
		}
		this.ccsClient.onPeerResponse('get-geo-location', resp => {
			ack(resp);
		});
	}

	/**
	 * 请求访客扫码
	 * @since 1.0.5版本更新API
	 * @param {object} cmdData 指令内容
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.requestCustomerScanCode(
			undefined,//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	requestCustomerScanCode(cmdData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		let askForPermission = false;
		this.ccsClient.sendPeerCmd('scan-code', askForPermission, cmdData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:requestCustomerScanCode,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 取消访客扫码
	 * @since 1.0.5版本更新API
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelCustomerScanCode(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('cancelCustomerScanCode success', res);
			},
			error => {
				console.log('cancelCustomerScanCode error', error);
			}
		);
	 */
	cancelCustomerScanCode(cmdId, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.sendPeerCancel('scan-code', cmdId, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:cancelCustomerScanCode,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 接收访客扫码的过程/结果
	 * @since 1.0.5版本更新API
	 * @param {function} ack 返回访客的响应结果
	 * @example
	 * ivcs.agent.onScanCode(data => {
		console.log(data);
		});
	 */
	onScanCode(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onScanCode error', isValid);
			return;
		}
		this.ccsClient.onPeerResponse('scan-code', resp => {
			ack(resp);
		});
	}

	/**
	 * 发送内容确认<目前用于AI坐席发送汇总确认消息>
	 * @since 1.0.5版本更新API
	 * @param {object} cmdData 指令内容 {type: enum('online-resource','text','form','xml'| ...)}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.sendConfirm(
			{type: enum('online-resource','text','form','xml'| ...)},//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	sendConfirm(cmdData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		let askForPermission = false;
		this.ccsClient.sendPeerCmd('confirm', askForPermission, cmdData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:sendConfirm,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 取消内容确认<目前用于AI坐席发送汇总确认消息>
	 * @since 1.0.5版本更新API
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.sendCancelConfirm(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('sendCancelConfirm success', res);
			},
			error => {
				console.log('sendCancelConfirm error', error);
			}
		);
	 */
	sendCancelConfirm(cmdId, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.sendPeerCancel('confirm', cmdId, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:sendCancelConfirm,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 接收访客查看内容确认过程/结果
	 * @since 1.0.5版本更新API
	 * @param {function} ack 返回访客的响应结果
	 * @example
	 * ivcs.agent.onConfirm(data => {
		  console.log(data);
		});
	 */
	onConfirm(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onConfirm error', isValid);
			return;
		}
		this.ccsClient.onPeerResponse('confirm', resp => {
			ack(resp);
		});
	}

	/**
	 * 发送选项判断<目前用于AI坐席发送选项判断消息>
	 * @since 1.0.5版本更新API
	 * @param {object} cmdData 指令内容 {title: string(Title),type: enum('simple-select'),options: array(object(Option):: {id: string(OptionId), description: string(OptionDesc)})}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.makeChoice(
			{title: string(Titile),type: enum('simple-select'),options: array(object(Option):: {id: string(OptionId), description: string(OptionDesc)})},//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	makeChoice(cmdData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		let askForPermission = false;
		this.ccsClient.sendPeerCmd('make-choice', askForPermission, cmdData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:makeChoice,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 取消选项判断<目前用于AI坐席发送选项判断消息>
	 * @since 1.0.5版本更新API
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelMakeChoice(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('cancelMakeChoice success', res);
			},
			error => {
				console.log('cancelMakeChoice error', error);
			}
		);
	 */
	cancelMakeChoice(cmdId, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.sendPeerCancel('make-choice', cmdId, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:cancelMakeChoice,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 接收访客选择过程/结果
	 * @since 1.0.5版本更新API
	 * @param {function} ack 返回访客的响应结果
	 * @example
	 *  ivcs.agent.onMakeChoice(data => {
		  console.log(data);
		});
	 */
	onMakeChoice(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onMakeChoice error', isValid);
			return;
		}
		this.ccsClient.onPeerResponse('make-choice', resp => {
			ack(resp);
		});
	}

	/**
	 * 开启同屏浏览<目前用于坐席端同屏辅助>
	 * @since 1.0.5版本更新API
	 * @param {object} cmdData 指令内容 默认是undefined
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.coBrowsing(
			undefined,//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	coBrowsing(cmdData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		let askForPermission = false;
		this.ccsClient.sendPeerCmd('co-browsing', askForPermission, cmdData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:coBrowsing,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 取消同屏浏览<目前用于坐席端同屏辅助>
	 * @since 1.0.5版本更新API
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelCoBrowsing(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('cancelCoBrowsing success', res);
			},
			error => {
				console.log('cancelCoBrowsing error', error);
			}
		);
	 */
	cancelCoBrowsing(cmdId, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.sendPeerCancel('co-browsing', cmdId, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:cancelCoBrowsing,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 接收访客同屏数据过程/结果<目前用于坐席端同屏辅助>
	 * @since 1.0.5版本更新API
	 * @param {function} ack 返回访客的响应结果
	 * @example
	 * ivcs.agent.onCoBrowsing(data => {
		console.log(data);
	   });
	 */
	onCoBrowsing(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onCoBrowsing error', isValid);
			return;
		}
		this.ccsClient.onPeerResponse('co-browsing', resp => {
			ack(resp);
		});
	}

	/**
	 * 同屏浏览标注<目前用于坐席端同屏辅助>
	 * @since 1.0.5版本更新API
	 * @param {object} cmdData 指令内容 object(FabricObj)
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.browsingMark(
			{},//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	browsingMark(cmdData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		let askForPermission = false;
		this.ccsClient.sendPeerCmd('browsing-mark', askForPermission, cmdData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:browsingMark,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 取消同屏浏览标注<目前用于坐席端同屏辅助>
	 * @since 1.0.5版本更新API
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelBrowsingMark(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('cancelBrowsingMark success', res);
			},
			error => {
				console.log('cancelBrowsingMark error', error);
			}
		);
	 */
	cancelBrowsingMark(cmdId, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.sendPeerCancel('browsing-mark', cmdId, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:cancelBrowsingMark,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 接收访客同屏标注过程/结果<目前用于坐席端同屏辅助>
	 * @since 1.0.5版本更新API
	 * @param {function} ack 返回访客的响应结果
	 * @example
	 * ivcs.agent.onBrowsingMark(data => {
		  console.log(data);
		});
	 */
	onBrowsingMark(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onBrowsingMark error', isValid);
			return;
		}
		this.ccsClient.onPeerResponse('browsing-mark', resp => {
			ack(resp);
		});
	}

	/**
	 * 返回推流的状态数据，可根据2次间隔之间的指标参数变化判断网络状态
	 * @param {number} milliseconds 返回的时间间隔，单位(毫秒)，默认3000毫秒。即每隔3秒返回实时流的相关状态数据
	 * @param {function} onSuccess {actualBitrate:实际码率(单位kbps),actualHeight:发送的视频分辨率的高,actualWidth:发送的视频分辨率的宽,bytesSent:一共发送的的字节数,packetsSent:一共发送的数据包数,packetsLost:一共丢的数据包数,rate:实时的视频帧率,packetsLostRate:丢包率}
	 * @param {function} onFailure code 错误说明{401:当前流未推送,402:已经正在获取推流数据中,无须再次调用,403:publication.getStats失败}
	 * @returns {number} intervalId 返回定时任务id,作为移除该任务的参数。
	 * @example
	 *  let milliseconds = 3000;
		ivcs.agent.getLocalStreamStats(
			milliseconds,
			(resp) => {
				console.log('getLocalStreamStats,success',resp);
			},
			(error) => {
				console.log('getLocalStreamStats,failed', error);
			}
		);
	 */
	getLocalStreamStats(milliseconds = 3000, onSuccess, onFailure) {
		return this.irtcClient.getPublicationStats(this.publication, milliseconds, onSuccess, onFailure);
	}

	/**
	 * 停止获取推流的状态数据
	 * @param {number} intervalId
	 * @param {function} onSuccess 'ok'
	 * @param {function} onFailure code 错误说明{401:当前流未推送，402:intervalId无效}
	 * @example
	 * ivcs.agent.stopLocalStreamStats(
			getStatsIntervalId,
			(resp) => {
				console.log('stopLocalStreamStats,success', resp, new Date());
			},
			(error) => {
				console.log('stopLocalStreamStats,failed', error);
			}
		);
	 */
	stopLocalStreamStats(intervalId, onSuccess, onFailure) {
		this.irtcClient.stopPublicationStats(this.publication, intervalId, onSuccess, onFailure);
	}

	/**
	 * 根据设备ID切换摄像头
	 * @param {string} deviceId
	 * @param {function} onSuccess 返回切换后的 MediaStream
	 * @param {function} onFailure 返回切换失败的回调
	   * @param { Function } onEnded  切换的画面视频帧结束，画面变黑，恢复则需要重新切换下摄像头。
	 * @example
	 * ivcs.agent.switchCameraWithDevice(
			deviceId,
			(resp) => {
				console.log('switch camera,success', resp, new Date());
			},
			(error) => {
				console.log('switch camera,failed', error);
			},
			(onEnded) => {
				console.log('switch camera,onEnded', onEnded);
			}
		);
	 */
	async switchCameraWithDevice(deviceId, onSuccess, onFailure, onEnded) {
		try {
			let stream = await this.mediaDevices.changeCameraWithDeviceId(deviceId);
			if (!this.localStream) {
				Logger.error('ivcs-js-sdk:switch camera with deviceId error localStream is null');
				return;
			}
			if (!stream) {
				Logger.error('ivcs-js-sdk:switch camera with deviceId error stream is null');
				return;
			}
			if (stream && stream.mediaStream && stream.mediaStream.getVideoTracks()) {
				stream.mediaStream.getVideoTracks()[0].onended = () => {
					onEnded();
				};
			} else if (stream && stream.getVideoTracks()) {
				stream.getVideoTracks()[0].onended = () => {
					onEnded();
				};
			}
			replaceMediaStream(
				this.localStream,
				stream,
				this.publication,
				resp => {
					this.localStream = stream;
					Logger.info('ivcs-js-sdk:switch camera with deviceId success' + resp);
					onSuccess(this.localStream);
				},
				error => {
					Logger.error('ivcs-js-sdk:switch camera with deviceId is failed' + error);
					onFailure(error);
				}
			);
		} catch (error) {
			Logger.error('ivcs-js-sdk:switch camera with deviceId is failed' + error);
			onFailure(error);
		}
	}

	/**
	 * 根据 MeidaConstraints 创建远端流
	 * @param {MediaTrackConstraints} deviceId
	 * @param {function} onSuccess 返回切换后的 MediaStream
	 * @param {function} onFailure 返回切换失败的回调
	 * @example
	 * ivcs.agent.createStreamWithMediaTrackConstraints(
			mediaConstraints,
			(success) => {
				console.log('create stream with media constraints, success', resp, new Date());
			},
			(error) => {
				console.log('create stream with media constraints, failed', error);
			}
		);
	 */
	createStreamWithMediaTrackConstraints(mediaConstraints, onSuccess, onFailure) {
		if (validateLocalStreamParameter(mediaConstraints) !== 'ok') {
			return;
		}
		const { audioConstraints, videoConstraints } = mediaConstraints;
		if (videoConstraints.deviceId) {
			this.videoInputDeviceID = videoConstraints.deviceId;
		}
		if (audioConstraints.deviceId) {
			this.audioInputDeviceID = audioConstraints.deviceId;
		}
		if (audioConstraints) {
			this.audioConstraints = audioConstraints;
		}

		if (videoConstraints) {
			this.videoConstraints = videoConstraints;
		}

		try {
			this.mediaDevices.createStream(
				this.irtcClient,
				this.audioInputDeviceID,
				this.videoInputDeviceID,
				this.audioConstraints,
				this.videoConstraints,
				stream => {
					this.publication
						.updateMedia(stream)
						.then(resp => {
							Logger.info('ivcs-js-sdk:create stream with media constraints, success', resp, new Date().toISOString());
							onSuccess(stream);
						})
						.catch(error => {
							Logger.error('ivcs-js-sdk:create stream with media constraints, error', error, new Date().toISOString());
							let err = new IvcsError('1023', error.message || error.name || error);
							onFailure(err);
						});
				},
				error => {
					Logger.error('ivcs-js-sdk:create stream with media constraints error', error, new Date().toISOString());
					let err = new IvcsError('1023', error.message || error.name || error);
					onFailure(error);
				}
			);
		} catch (error) {
			Logger.error('ivcs-js-sdk:create stream with media constraints error', error, new Date().toISOString());
			let err = new IvcsError('1023', error.message || error.name || error);
			onFailure(err);
		}
	}

	/**
	 * 根据设备ID切换麦克风
	 * @param {string} deviceId
	 * @param {function} onSuccess 返回切换后的 MediaStream
	 * @param {function} onFailure 返回切换失败的回调
	 * @param { function } onEnded  切换的画面视频帧结束，画面变黑，恢复则需要重新切换下摄像头。
	 * @example
	 * ivcs.agent.switchMicWithDevice(
			deviceId,
			(resp) => {
				console.log('switch mic,success', resp, new Date());
			},
			(error) => {
				console.log('switch mic,failed', error);
			},
			(onEnded) => {
				console.log('switch mic,onEnded', onEnded);
			}
		);
	 */
	async switchMicWithDevice(deviceId, onSuccess, onFailure, onEnded) {
		try {
			let stream = await this.mediaDevices.changeMicWithDeviceId(deviceId);
			if (!this.localStream) {
				Logger.error('ivcs-js-sdk:switch mic with deviceId error localStream is null');
				return;
			}
			if (!stream) {
				Logger.error('ivcs-js-sdk:switch mic with deviceId error stream is null');
				return;
			}
			if (stream && stream.mediaStream && stream.mediaStream.getVideoTracks()) {
				stream.mediaStream.getVideoTracks()[0].onended = () => {
					onEnded();
				};
			} else if (stream && stream.getVideoTracks()) {
				stream.getVideoTracks()[0].onended = () => {
					onEnded();
				};
			}
			replaceMediaStream(
				this.localStream,
				stream,
				this.publication,
				resp => {
					this.localStream = stream;
					Logger.info('ivcs-js-sdk:switch mic with deviceId success' + resp);
					onSuccess(this.localStream);
				},
				error => {
					Logger.error('ivcs-js-sdk:switch mic with deviceId is failed' + error);
					onFailure(error);
				}
			);
		} catch (error) {
			Logger.error('ivcs-js-sdk:switch mic with deviceId is failed' + error);
			onFailure(error);
		}
	}

	/**
	 * 手机端实现前后摄像头切换
	 * IOS 采用 facingMode 方式切换
	 * Android 采用 enumerateDevices 方式切换
	 * @param { function } onSuccess  切换成功，返回切换成功后得 mediaStream
	 * @param { function } onFailure  切换失败，返回失败原因 {1023:切换流失败}
	 * @param { function } onEnded  切换的画面视频帧结束，画面变黑，恢复则需要重新切换下摄像头。
	 * @param { string }   videoId 自己画面video标签的id，默认为空，不传则需要自己去拿到mediaStream去渲染,支持传多个标签，格式'videoId1,videoId2'
	 * @param { function } onplayError 切换后自动播放失败
	 */
	switchCamera(onSuccess, onFailure, onEnded, videoId = '', onplayError) {
		stopStream(this.localStream);
		this.mediaDevices.switchCameraGetDeviceId(deviceId => {
			const audioDeviceId = null;
			const videoDeviceId = deviceId;
			const audioConstraints = this.mediaOptions.cameraStreamAudioConstraints;
			const videoConstraints = this.mediaOptions.cameraStreamVideoConstraints;
			this.mediaDevices.createStream(
				this.irtcClient,
				audioDeviceId,
				videoDeviceId,
				audioConstraints,
				videoConstraints,
				stream => {
					this.publication
						.updateMedia(stream)
						.then(resp => {
							Logger.info('ivcs-js-sdk:switchCamera success', resp, new Date().toISOString());
							onSuccess(stream);
						})
						.catch(error => {
							Logger.error('ivcs-js-sdk:switchCamera error', error, new Date().toISOString());
							let err = new IvcsError('1023', error.message || error.name || error);
							onFailure(err);
						});
					Logger.info('ivcs-js-sdk:switchCamera render videoId', videoId);
					if (videoId) {
						const isMeStream = true;
						let videoIds = videoId.split(',');
						videoIds &&
							videoIds.forEach(item => {
								renderStreamWithVideoLabel(stream, item, isMeStream, onplayError, onEnded)
									.then()
									.catch(error => {
										Logger.info(`ivcs-js-sdk:switchCamera render videoId:${videoId} error, ${error}`);
										onFailure(error);
									});
							});
					}
					if (stream && stream.mediaStream && stream.mediaStream.getVideoTracks()) {
						stream.mediaStream.getVideoTracks()[0].onended = () => {
							onEnded();
						};
					} else if (stream && stream.getVideoTracks()) {
						stream.getVideoTracks()[0].onended = () => {
							onEnded();
						};
					}
				},
				error => {
					onFailure(error);
				}
			);
		}, onFailure);
	}

	/**
	 * 根本参数修改本地流
	 * @param {object} constraints 视频流参数
	 * @param {function} onSuccess 成功回调
	 * @param {function} onFailure 失败回调
	 * @returns 返回应用后的流
	 * @example
	 * ivcs.agent.changeConstraintsWidthStream(
			constraints,
			(resp) => {
				console.log('change constraints,success', resp, new Date());
			},
			(error) => {
				console.log('change constraints', error);
			}
		);
	*/
	async changeConstraintsWidthStream(constraints, onSuccess, onFailure) {
		if (!this.localStream) {
			Logger.error('ivcs-js-sdk:change constraints failed localStream is null');
			return;
		}
		try {
			let stream = await this.mediaDevices.changeConstraints(this.localStream, constraints);
			if (!stream) {
				Logger.error('ivcs-js-sdk:change constraints failed stream is null');
				return;
			}
			replaceMediaStream(
				this.localStream,
				stream,
				this.publication,
				resp => {
					this.localStream = stream;
					Logger.info('ivcs-js-sdk:change constraints  success' + resp);
					onSuccess(stream);
				},
				error => {
					Logger.error('ivcs-js-sdk:change constraints  is failed' + error);
					onFailure(error);
				},
				false
			);
		} catch (error) {
			onFailure(error);
		}
	}

	/**
	 * 以特定from_clientId 发送消息
	 * @ignore
	 * @param {string} message 消息内容
	 * @param {string} from  为空，则默认是当前用户发送的消息
	 * @param {string} toRole 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 */
	sendTextMessageWithFromClientId(message, from, toRole = 'customers', onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.sendMessageFromTo('text', message, from, toRole, (status, resp) => {
			if (status === 'ok') {
				onSuccess();
			} else {
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 切换视频流,可用于虚拟背景切换原生流，或者其他新创建的流。
	 * @param {string} stream
	 * @param {function} onSuccess 返回切换后的 MediaStream
	 * @param {function} onFailure 返回切换失败的回调
	 * @param {function} onEnded  切换的画面视频帧结束
	 * @example
	 * ivcs.agent.switchCameraWithStream(
			stream,
			(resp) => {
				console.log('switch camera,success', resp, new Date());
			},
			(error) => {
				console.log('switch camera,failed', error);
			},
			(onEnded) => {
				console.log('switch camera,onEnded', onEnded);
			}
		);
	 */
	async switchCameraWithStream(stream, onSuccess, onFailure, onEnded) {
		try {
			if (!this.localStream) {
				Logger.error('ivcs-js-sdk:switch camera with stream error localStream is null');
				return;
			}
			if (!stream) {
				Logger.error('ivcs-js-sdk:switch camera with stream error stream is null');
				return;
			}
			let newMediaStream = stream;
			if (stream && stream.mediaStream && stream.mediaStream.getVideoTracks()) {
				newMediaStream = stream.mediaStream;
				stream.mediaStream.getVideoTracks()[0].onended = () => {
					onEnded();
				};
			} else if (stream && stream.getVideoTracks()) {
				stream.getVideoTracks()[0].onended = () => {
					onEnded();
				};
			}
			this.publication
				.updateMedia(stream)
				.then(resp => {
					Logger.info('ivcs-js-sdk:switch camera with stream success', resp);
					onSuccess('ok');
				})
				.catch(error => {
					Logger.error('ivcs-js-sdk:switch camera with stream error', error);
					let err = new IvcsError('1023', error.message || error.name || error);
					onFailure(err);
				});
		} catch (error) {
			Logger.error('ivcs-js-sdk:switch camera with stream is failed' + error);
			onFailure(error);
		}
	}

	/**
	 * 开启共享屏幕标注，记录留痕管理后台
	 * @ignore
	 * @param {string|object} data 默认undefined
	 * @param {string} toWhom 发送给谁 clientId具体的用户 'customers'= 访客;'agents'= 坐席;'others'= 除了自己的所有人; 'all' = 所有人;
	 * @param {function} ack
	 */
	screenShareMarkDrawing(data, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.screenShareMarkDrawing(data, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:screenShareMarkDrawing,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 停止共享屏幕标注，记录留痕管理后台
	 * @ignore
	 * @param {string|object} data 默认undefined
	 * @param {string} toWhom 发送给谁 clientId具体的用户 'customers'= 访客;'agents'= 坐席;'others'= 除了自己的所有人; 'all' = 所有人;
	 * @param {function} ack
	 */
	stopScreenShareMarkDrawing(data, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.stopScreenShareMarkDrawing(data, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:stopScreenShareMarkDrawing,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 获取流音量大小，根据audio
	 * @param {string} videoId audio&video标签
	 * @param {function} onSuccess 请求成功
	 * @param {function} onFailure {code,message} '600':'无音视频会话','500':'videoId不存在元素','501':'videoId元素无srcObject'，‘502’:'缺少参数，onSuccess或者onFailure或者cb'
	 * @param {function} cb  返回音量大小 (0-200);
	 * @example
	 * ivcs.agent.getAudioLevel('agent-001',(s)=>{},(e)=>{},(volume)=>{})
	 */
	getAudioLevel(videoId, onSuccess, onFailure, cb) {
		if (!this.irtcClient) {
			onFailure('600', 'not join Room');
			return;
		}
		this.irtcClient.getAudioLevel(videoId, onSuccess, onFailure, cb);
	}

	/**
	 * 用于一般静音某个音视频流后，暂停音频上下文中的时间进程，暂停音频硬件访问并减少进程中的 CPU/电池使用。ps:请结合getAudioLevel使用,本api已丢弃
	 * @ignore
	 * @param {string} videoId audio&video标签
	 * @param {function} onSuccess 请求成功
	 * @param {function} onFailure {code,message} '600':'无音视频会话','500':'videoId不存在元素','501':'未监听该videoId的音量大小，未执行getAudioLevel'，‘502’:'操作失败'
	 * @example
	 * ivcs.agent.suspendGetAudioLevel('agent-001',(s)=>{},(e)=>{})
	 */
	suspendGetAudioLevel(videoId, onSuccess, onFailure) {
		if (!this.irtcClient) {
			onFailure('600', 'not join Room');
			return;
		}
		this.irtcClient.suspendGetAudioLevel(videoId, onSuccess, onFailure);
	}

	/**
	 * 用于一般恢复某个音视频流声音《取消静音》后，恢复之前被暂停的音频上下文中的时间进程。ps:请结合getAudioLevel使用，本api已丢弃
	 * @ignore
	 * @param {string} videoId audio&video标签
	 * @param {function} onSuccess 请求成功
	 * @param {function} onFailure {code,message} '600':'无音视频会话','500':'videoId不存在元素','501':'未监听该videoId的音量大小，未执行getAudioLevel'，‘502’:'操作失败'
	 * @example
	 * ivcs.agent.resumeGetAudioLevel('agent-001',(s)=>{},(e)=>{})
	 */
	resumeGetAudioLevel(videoId, onSuccess, onFailure) {
		if (!this.irtcClient) {
			onFailure('600', 'not join Room');
			return;
		}
		this.irtcClient.resumeGetAudioLevel(videoId, onSuccess, onFailure);
	}

	/**
	 * 发送ocr识别结果
	 * @param {object} data ocr识别结果 {type: enum('idcard','bankcard'),result:object()}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功  'ok'
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.sendOcrRecord(
			{type: enum('idcard','bankcard'),result:object()},
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	sendOcrRecord(data, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.sendOcrRecord(data, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:sendOcrRecord,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 采集身份证正反面
	 * @param {object} cmdData 指令内容 默认为{},表明采集正反面
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.collectIdCard(
			{},//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	collectIdCard(cmdData, toWhom, onSuccess, onFailure) {
		let askForPermission = false;
		sendPeerCmd(this.ccsClient, 'idcard-collection', askForPermission, cmdData, toWhom, onSuccess, onFailure);
	}

	/**
	 * 取消身份证采集
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelIdCardCollection(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('cancelIdCardCollection success', res);
			},
			error => {
				console.log('cancelIdCardCollection error', error);
			}
		);
		*/
	cancelIdCardCollection(cmdId, toWhom, onSuccess, onFailure) {
		cancelPeerCmd(this.ccsClient, 'idcard-collection', cmdId, toWhom, onSuccess, onFailure);
	}

	/**
	 * 接收访客身份证采集结果
	 * @param {function} ack 返回访客的响应结果 {status:'accepted','rejected','in-progress','failed','done','canceled'. data:{urls:[]} }
	 * @example
	 * ivcs.agent.onIdCardCollectResult(data => {
			 console.log(data);
		});
	*/
	onIdCardCollectResult(ack) {
		onPeerCmdResponse(this.ccsClient, 'idcard-collection', ack);
	}

	/**
	 * 采集银行卡
	 * @param {object} cmdData 指令内容 默认为{},表明采集正反面
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.collectBankCard(
			{},//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	collectBankCard(cmdData, toWhom, onSuccess, onFailure) {
		let askForPermission = false;
		sendPeerCmd(this.ccsClient, 'bankcard-collection', askForPermission, cmdData, toWhom, onSuccess, onFailure);
	}

	/**
	 * 取消银行卡采集
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelBankCardCollection(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('cancelBankCardCollection success', res);
			},
			error => {
				console.log('cancelBankCardCollection error', error);
			}
		);
		*/
	cancelBankCardCollection(cmdId, toWhom, onSuccess, onFailure) {
		cancelPeerCmd(this.ccsClient, 'bankcard-collection', cmdId, toWhom, onSuccess, onFailure);
	}

	/**
	 * 接收访客银行卡采集结果
	 * @param {function} ack 返回访客的响应结果 {status:'accepted','rejected','in-progress','failed','done','canceled'. data:{urls:[]} }
	 * @example
	 * ivcs.agent.onBankCardCollectResult(data => {
			 console.log(data);
		});
	*/
	onBankCardCollectResult(ack) {
		onPeerCmdResponse(this.ccsClient, 'bankcard-collection', ack);
	}

	/**
	 * 监听到机器人开始播报一句话术
	 * @param {function} ack {status,resp,msgId}
	 * @example
	 * ivcs.agent.onAiStartSpeak((status,resp,msgId) => {
		});
	 */
	onAiStartSpeak(ack) {
		this.ccsClient.onAiStartSpeak(data => {
			ack(data);
		});
	}

	/**
	 * 监听到机器人一句话术播报完成
	 * @param {function} ack {status,resp,msgId}
	 * @example
	 * ivcs.agent.onAiStartDone((status,resp,msgId) => {
		});
	 */
	onAiStartDone(ack) {
		this.ccsClient.onAiStartDone(data => {
			ack(data);
		});
	}

	/**
	 * 开启白板
	 * @param {string} name 白板名称，可任意填写。
	 * @param {string} path 白板的前端页面部署的path, 如：'/customer/static/whiteboard'。
	 * @param {string} hostname 白板的前端部署主机地址, 如：'https://www.abc.com'。
	 * @param {function} onSuccess {whiteboardId,iframe}
	 * @param {function} onFailure {code,message}
	 */
	async startWhiteboard(name, path, hostname, onSuccess, onFailure) {
		if (!this.irtcClient) {
			Logger.error('ivcs-js-sdk:not join room');
			return;
		}
		try {
			!this.irtcWhiteBoard ? (this.irtcWhiteBoard = new IrtcWhiteBoard(this.irtcClient)) : '';
			let whiteboard = await this.irtcWhiteBoard.create(name, path);
			let iframe = await this.irtcWhiteBoard.start(whiteboard, hostname);
			onSuccess({
				whiteboardId: whiteboard.id,
				iframe: iframe,
			});
		} catch (error) {
			Logger.error(`ivcs-js-sdk:startWhiteboard error,at:${new Date().toISOString()}`);
			onFailure(error);
		}
	}

	/**
	 * 删除白板
	 * @param {string} whiteboardId 白板id
	 * @param {function} onSuccess  成功‘ok’
	 * @param {function} onFailure  失败{code,message}
	 */
	deleteWhiteboard(whiteboardId, onSuccess, onFailure) {
		if (!this.irtcClient) {
			Logger.error('ivcs-js-sdk:not join room');
			return;
		}
		if (!this.irtcWhiteBoard) {
			Logger.error('ivcs-js-sdk:not startWhiteboard');
			return;
		}
		this.irtcWhiteBoard.removeWithId(whiteboardId, onSuccess, onFailure);
	}

	/**
	 * 开启pdf白板
	 * @param {string} name pdf白板名称，可任意填写。
	 * @param {string} path 白板的前端页面部署的path, 如：'/customer/static/whiteboard'。
	 * @param {string} hostname 白板的前端部署主机地址, 如：'https://www.abc.com'。
	 * @param {function} onSuccess {whiteboardId,iframe}
	 * @param {function} onFailure {code,message}
	 */
	async startPdfWhiteboard(name, path, hostname, onSuccess, onFailure) {
		this.startWhiteboard(name, path, hostname, onSuccess, onFailure);
	}

	/**
	 * 删除pdf白板
	 * @param {string} whiteboardId 白板id
	 * @param {function} onSuccess  成功‘ok’
	 * @param {function} onFailure  失败{code,message}
	 */
	deletePdfWhiteboard(whiteboardId, onSuccess, onFailure) {
		this.deleteWhiteboard(whiteboardId, onSuccess, onFailure);
	}

	/**
	 * 开启证言抄录白板
	 * @param {string} name 证言抄录白板白板名称，可任意填写。
	 * @param {string} path 白板的前端页面部署的path, 如：'/customer/static/whiteboard'。
	 * @param {string} hostname 白板的前端部署主机地址, 如：'https://www.abc.com'。
	 * @param {function} onSuccess {whiteboardId,iframe}
	 * @param {function} onFailure {code,message}
	 */
	async startTestimonyWrite(name, path, hostname, onSuccess, onFailure) {
		this.startWhiteboard(name, path, hostname, onSuccess, onFailure);
	}

	/**
	 * 删除证言抄录白板
	 * @param {string} whiteboardId 白板id
	 * @param {function} onSuccess  成功‘ok’
	 * @param {function} onFailure  失败{code,message}
	 */
	deleteTestimonyWrite(whiteboardId, onSuccess, onFailure) {
		this.deleteWhiteboard(whiteboardId, onSuccess, onFailure);
	}

	/**
	 * 采集变量
	 * @param {object} cmdData 指令内容  [{key,desc,property},{key2,desc2,property2},...]
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.collectVariable(
			[],//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	collectVariable(cmdData, toWhom, onSuccess, onFailure) {
		let askForPermission = false;
		sendPeerCmd(this.ccsClient, 'collect-variable', askForPermission, cmdData, toWhom, onSuccess, onFailure);
	}

	/**
	 * 取消变量采集
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelCollectVariable(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('cancelCollectVariable success', res);
			},
			error => {
				console.log('cancelCollectVariable error', error);
			}
		);
		*/
	cancelCollectVariable(cmdId, toWhom, onSuccess, onFailure) {
		cancelPeerCmd(this.ccsClient, 'collect-variable', cmdId, toWhom, onSuccess, onFailure);
	}

	/**
	 * 接收访客变量采集的过程/结果
	 * @param {function} ack 返回访客的响应结果
	 * @example
	 * ivcs.agent.onCollectVariableResult(data => {
			 console.log(data);
		});
	*/
	onCollectVariableResult(ack) {
		onPeerCmdResponse(this.ccsClient, 'collect-variable', ack);
	}

	/**
	 * 发送共享白板指令
	 * @param {object} cmdData 指令内容  {whiteboardId:string('')} . 前绪开启共享白板成功后返回的whiteboardId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.sendWhiteboard(
			{},//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	sendWhiteboard(cmdData, toWhom, onSuccess, onFailure) {
		let askForPermission = false;
		sendPeerCmd(this.ccsClient, 'whiteboard', askForPermission, cmdData, toWhom, onSuccess, onFailure);
	}

	/**
	 * 取消共享白板指令
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelWhiteboard(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('cancelWhiteboard success', res);
			},
			error => {
				console.log('cancelWhiteboard error', error);
			}
		);
		*/
	cancelWhiteboard(cmdId, toWhom, onSuccess, onFailure) {
		cancelPeerCmd(this.ccsClient, 'whiteboard', cmdId, toWhom, onSuccess, onFailure);
	}

	/**
	 * 接收访客共享白板的过程/结果
	 * @param {function} ack 返回访客的响应结果
	 * @example
	 * ivcs.agent.onWhiteboardResult(data => {
			console.log(data);
		});
	*/
	onWhiteboardResult(ack) {
		onPeerCmdResponse(this.ccsClient, 'whiteboard', ack);
	}

	/**
	 * 发送共享pdf白板指令
	 * @param {object} cmdData 指令内容  {whiteboardId:string('')} . 前绪开启共享pdf白板成功后返回的whiteboardId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.sendPdfWhiteboard(
			{},//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	sendPdfWhiteboard(cmdData, toWhom, onSuccess, onFailure) {
		let askForPermission = false;
		sendPeerCmd(this.ccsClient, 'whiteboard-pdf', askForPermission, cmdData, toWhom, onSuccess, onFailure);
	}

	/**
	 * 取消共享pdf白板指令
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelPdfWhiteboard(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('cancelPdfWhiteboard success', res);
			},
			error => {
				console.log('cancelPdfWhiteboard error', error);
			}
		);
		*/
	cancelPdfWhiteboard(cmdId, toWhom, onSuccess, onFailure) {
		cancelPeerCmd(this.ccsClient, 'whiteboard-pdf', cmdId, toWhom, onSuccess, onFailure);
	}

	/**
	 * 接收访客共享pdf白板的过程/结果
	 * @param {function} ack 返回访客的响应结果
	 * @example
	 * ivcs.agent.onPdfWhiteboardResult(data => {
			console.log(data);
		});
	*/
	onPdfWhiteboardResult(ack) {
		onPeerCmdResponse(this.ccsClient, 'whiteboard-pdf', ack);
	}

	/**
	 * 发送证言抄录
	 * @param {object} cmdData 指令内容  {content:string('')}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.sendTestimonyWrite(
			{},//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	sendTestimonyWrite(cmdData, toWhom, onSuccess, onFailure) {
		let askForPermission = false;
		sendPeerCmd(this.ccsClient, 'testimony-write', askForPermission, cmdData, toWhom, onSuccess, onFailure);
	}

	/**
	 * 取消证言抄录
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelTestimonyWrite(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('cancelTestimonyWrite success', res);
			},
			error => {
				console.log('cancelTestimonyWrite error', error);
			}
		);
		*/
	cancelTestimonyWrite(cmdId, toWhom, onSuccess, onFailure) {
		cancelPeerCmd(this.ccsClient, 'testimony-write', cmdId, toWhom, onSuccess, onFailure);
	}

	/**
	 * 接收访客证言抄录的过程/结果
	 * @param {function} ack 返回访客的响应结果
	 * @example
	 * ivcs.agent.onTestimonyWriteResult(data => {
			console.log(data);
		});
	*/
	onTestimonyWriteResult(ack) {
		onPeerCmdResponse(this.ccsClient, 'testimony-write', ack);
	}

	/**
	 * 短视频采集
	 * @param {object} cmdData 指令内容  {time:int('30')ms} 采集时长，单位ms
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.collectShortVideo(
			{time:30},//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	collectShortVideo(cmdData, toWhom, onSuccess, onFailure) {
		let askForPermission = false;
		sendPeerCmd(this.ccsClient, 'collect-short-video', askForPermission, cmdData, toWhom, onSuccess, onFailure);
	}

	/**
	 * 取消短视频采集
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelCollectShortVideo(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('cancelTestimonyWrite success', res);
			},
			error => {
				console.log('cancelTestimonyWrite error', error);
			}
		);
		*/
	cancelCollectShortVideo(cmdId, toWhom, onSuccess, onFailure) {
		cancelPeerCmd(this.ccsClient, 'collect-short-video', cmdId, toWhom, onSuccess, onFailure);
	}

	/**
	 * 接收短视频采集的过程/结果
	 * @param {function} ack 返回访客的响应结果 拒绝：{reason:'',code:''} ，接受并成功拍摄：{url:string('url')}
	 * @example
	 * ivcs.agent.onCollectShortVideoResult(data => {
			console.log(data);
		});
	*/
	onCollectShortVideoResult(ack) {
		onPeerCmdResponse(this.ccsClient, 'collect-short-video', ack);
	}

	/**
	 * 设置资源文件的内外网替换地址
	 * @param {string} resourceOriginUrlDomain 'ivcs-nginx'
	 * @param {string} resourceReplaceUrlDomain '公网-domain'，	'内网-domain'
	 */
	setReplaceOrigin(resourceOriginUrlDomain, resourceReplaceUrlDomain) {
		if (!this.ccsClient) {
			console.error('setReplaceOrigin error,no ccs client');
			return;
		}
		this.ccsClient.setReplaceOrigin(resourceOriginUrlDomain, resourceReplaceUrlDomain);
	}

	/**
	 * 请求升级
	 * @param {string} media "media": "video"/ "audio",
	 * @param {string} toWhom 发送给谁 clientId具体的用户 'customers'= 访客;'agents'= 坐席;'others'= 除了自己的所有人; 'all' = 所有人;
	 * @param {function} onSuccess
	 * @param {function} onFailure {code:60000<音视频创建出错>}
	 */
	requestUpgrade(media, toWhom, onSuccess, onFailure) {
		this.imUpgradeMedia.requestUpgrade(media, toWhom, onSuccess, onFailure, this.ccsClient);
	}

	/**
	 * 接受对方的升级请求
	 * @param {string} media "media": "video"/ "audio",
	 * @param {function} onSuccess
	 * @param {function} onFailure {code:'60000',message:AbortError［中止错误］;NotAllowedError［拒绝错误］;NotFoundError［找不到错误］;NotReadableError［无法读取错误］;OverconstrainedError［无法满足要求错误］;SecurityError［安全错误］}
	 */
	acceptUpgrade(media, onSuccess, onFailure) {
		this.imUpgradeMedia.accept(
			media,
			data => {
				this.roomId = data?.roomId;
				this.roomToken = data?.roomToken;
				this.systemRecordId = data?.recordingId;
				this.recordingIds.push(this.systemRecordId);
				onSuccess(data);
			},
			onFailure,
			this.ccsClient
		);
	}

	/**
	 * 拒绝接听
	 * @param {string} media "media": "video"/ "audio",
	 * @param {object} reason {code,message}
	 * @param {function} onSuccess
	 * @param {function} onFailure
	 */
	rejectUpgrade(media, reason, onSuccess, onFailure) {
		this.imUpgradeMedia.reject(media, reason, onSuccess, onFailure, this.ccsClient);
	}

	/**
	 * 取消请求升级
	 * @param {string} reason 默认‘normal’
	 * @param {function} onSuccess
	 * @param {function} onFailure
	 */
	cancelUpgrade(reason = 'normal', onSuccess, onFailure) {
		this.imUpgradeMedia.cancel(reason, onSuccess, onFailure, this.ccsClient);
	}

	/**
	 * 监听升降级的事件
	 * @param {function} ack {type:'request-upgrade'|'accept-upgrade'|'reject-upgrade'|'cancel-upgrade',from:{},to:{},data:{media,roomId,roomToken,reason}}
	 */
	onUpgrade(ack) {
		this.imUpgradeMedia.onUpgrade(data => {
			Logger.info('ivcs-js-sdk:onUpgrade data', data);
			if (data?.data?.type === 'accept-upgrade') {
				this.roomId = data?.data?.roomId;
				this.roomToken = data?.data?.roomToken;
				this.systemRecordId = data?.data?.recordingId;
				this.recordingIds.push(this.systemRecordId);
				Logger.info('ivcs-js-sdk:onUpgrade room', this.roomId, this.roomToken);
			}
			ack(data);
		}, this.ccsClient);
	}

	/**
	 * 发送人工工作流节点完成情况
	 * @param {object} message {humanflowId:(人工工作流ID),step:(人工工作流执行步骤ID),at:(时间)}
	 * @param {string} toRole  发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 成功回调
	 * @param {function} onFailure 失败回调
	 * @example
	 * ivcs.agent.sendHumanStepDetail(
				{humanflowId:1,// 人工工作流IDstep:11 // 人工工作流执行步骤IDat:"" // 时间}
				'agents',
				res => {
					console.log('sendHumanStepDetail success',res);
				},
				error => {
					console.log('sendHumanStepDetail error',error);
				}
			);
	*/
	sendHumanStepDetail(message, toRole = 'agents', onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.sendHumanStepDetail(message, toRole, (status, resp, msgId) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error('ivcs-js-sdk:sendHumanStepDetail error', status, resp, msgId);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 收到人工工作流节点完成情况
	 * @param {function} ack  返回具体的节点完成数据  {humanflowId:(人工工作流ID),step:(人工工作流执行步骤ID),at:(时间)}
	 * @example
	 * ivcs.agent.onHumanStepDetail(data => {
			console.log('onHumanStepDetail data', data);
		});
	 */
	onHumanStepDetail(ack) {
		this.ccsClient.onHumanStepDetail(data => {
			ack(data);
		});
	}

	/**
	 * 对访客画面实时标注
	 * @param {object} cmdData 指令内容 默认为{},表明采集正反面
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功 {cmdId:string('cmdIdOfMsg'),createdAt:date()}
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.startCustomerMark(
			{},//指令内容
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			resp => {
				console.log(resp);
			},
			error => {
				console.log(resp);
			}
		);
	 */
	customerViewMark(cmdData, toWhom, onSuccess, onFailure) {
		let askForPermission = false;
		sendPeerCmd(this.ccsClient, 'customer-view-mark', askForPermission, cmdData, toWhom, onSuccess, onFailure);
	}

	/**
	 * 取消访客画面实时标注
	 * @param {string} cmdId 前序发送的业务指令后返回的cmdId
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.agent.cancelCustomerViewMark(
			cmdId,//前序发送的业务指令后返回的cmdId
			clientId,//发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
			res => {
				console.log('cancelCustomerViewMark success', res);
			},
			error => {
				console.log('cancelCustomerViewMark error', error);
			}
		);
	*/
	cancelCustomerViewMark(cmdId, toWhom, onSuccess, onFailure) {
		cancelPeerCmd(this.ccsClient, 'customer-view-mark', cmdId, toWhom, onSuccess, onFailure);
	}

	/**
	 * 接收访客针对实时标注的响应，接受/拒绝
	 * @param {function} ack 返回访客的响应结果{status:'accepted'|| 'rejected'}
	 * @example
	 * ivcs.agent.onCustomerViewMarkResult(data => {
			console.log(data);
		});
	*/
	onCustomerViewMarkResult(ack) {
		onPeerCmdResponse(this.ccsClient, 'customer-view-mark', ack);
	}

	/**
	 * 根据dom元素截取屏幕快照
	 * @param {object} element dom元素，如document.querySelector("#content"),document.body等
	 * @param {object} options 截屏参数 如:{ x: 370,y: 0,width: 270,height: 480,useCORS:true,},具体参数参考文档https://html2canvas.hertzen.com/configuration
	 * @param {function} onSuccess 成功 {imgCanvas:截图的canvas，pictureId:添加到录像文件后的图片id，可用于移除}
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.agent.snapshot(
			document.body,
			{
				width: window.screen.availWidth, 
				height: window.screen.availHeight,
				x: 0,
				y: 0,
			},
			true,
			{},
			(resp) => {
				console.log('snapshot success', resp);
				document.body.appendChild(resp.imgCanvas);
			},
			(error) => {
				console.log('snapshot error', error);
			}
		);
	 */
	async snapshot(element, options, onSuccess, onFailure) {
		try {
			let imgCanvas = await this.snapshotModule.getScreenshots(element, options);
			onSuccess({
				imgCanvas: imgCanvas,
			});
		} catch (error) {
			Logger.error('ivcs-js-sdk:snapshot error', error);
			onFailure(error);
		}
	}

	/**
	 * 坐席完成会话小结内容，通知给会话中的其他坐席
	 * @param {object} data  {"bizSummaryId": 1,  //会话小结配置项ID "name": "字段名称", // 字段名称"type": "textarea", // 类型"data": {}, //选项配置内容 "value": {}, // 选项值 "isRequired": "Y"  // 是否必填  N}
	 * @param {string} toWhom 发送给谁 clientId具体的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人;
	 * @param {function} onSuccess 发送成功  'ok'
	 * @param {function} onFailure 发送失败
	 */
	syncSessionSummary(data, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.syncSessionSummary(data, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.error(`ivcs-js-sdk:syncSessionSummary,error:${resp},at:${new Date()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 监听当前会话的会话小结的内容变化，其他坐席做的会话小结变更，都会通知到当前坐席
	 * @param {function} ack {"bizSummaryId": 1,  //会话小结配置项ID "name": "字段名称", // 字段名称"type": "textarea", // 类型"data": {}, //选项配置内容 "value": {}, // 选项值 "isRequired": "Y"  // 是否必填  N}
	 */
	onSyncSessionSummary(ack) {
		this.ccsClient.onSyncSessionSummary(data => {
			ack(data);
		});
	}
}

export { Agent };
